ThreadLocal 的介紹
ThreadLocal 是一個線程內部的數據存儲類,通過它可以在指定的線程中存儲數據食寡,數據存儲以后,只有在指定線程中可以獲取到存儲的數據,對于其他線程來說無法獲取到數據祖娘。Looper、ActivityThread 以及 AMS 中都用到了 ThreadLocal啊奄。
與 Synchronized 的比較
ThreadLocal 和 Synchronized 都用于解決多線程并發(fā)訪問渐苏∠瞥保可是 ThreadLocal 與 synchronized 有本質的差別。synchronized 是利用鎖的機制琼富,使變量或代碼塊 在某一時該僅僅能被一個線程訪問仪吧。而 ThreadLocal 為每個線程都提供了變量的副本,使得每個線程在某一時間訪問到的并非同一個對象鞠眉,這樣就隔離了多個線程對數據的數據共享薯鼠。
使用場景
- 當某些數據是以線程為作用域并且不同線程具有不同的數據副本的時候,就可以采用 ThreadLocal械蹋。比如對于 Handler 來說出皇,它需要獲取當前線程的 Looper,很顯然 Looper 的作用域就是線程并且不同線程具有不同的 Looper哗戈,這個時候通過 ThreadLocal 就可以輕松實現 Looper 在線程中的存取恶迈。
- 復雜邏輯下的對象傳遞,比如監(jiān)聽器的傳遞谱醇,有些時候一個線程中的任務過于復雜暇仲,這可能表現為函數調用棧比較深以及代碼入口的多樣性,在這種情況下副渴,我們又需要監(jiān)聽器能夠貫穿整個線程的執(zhí)行過程奈附,這時可以采用 ThreadLocal,采用 ThreadLocal 可以讓監(jiān)聽器作為線程內的全局對象而存在煮剧,在線程內部只要通過 get 方法就可以獲取到監(jiān)聽器斥滤。
使用方法及原理
ThreadLocal 類接口很簡單,只有 4 個方法勉盅,我們先來了解一下:
- void set(Object value)
設置當前線程的線程局部變量的值佑颇。 - public Object get()
該方法返回當前線程所對應的線程局部變量。 - public void remove()
將當前線程局部變量的值刪除草娜,目的是為了減少內存的占用挑胸,該方法是 JDK 5.0 新增的方法。需要指出的是宰闰,當線程結束后茬贵,對應該線程的局部變量將自動被垃圾回收,所以顯式調用該方法清除線程的局部變量并不是必須的操作移袍,但它可以加快內存回收的速度解藻。 - protected Object initialValue()
返回該線程局部變量的初始值,該方法是一個 protected 的方法葡盗,顯然是為了讓子類覆蓋而設計的螟左。這個方法是一個延遲調用方法,在線程第 1 次調用 get() 或 set(Object)時才執(zhí)行,并且僅執(zhí)行 1 次胶背。ThreadLocal 中的缺省實現直接返回一 個 null虫啥。
下面通過例子來說明,首先定義一個 ThreadLocal 對象奄妨,選擇 Boolean 類型涂籽,如下所示
private ThreadLocal<Boolean> mThreadLocal = new ThreadLocal<>();
然后分別在主線程、子線程1和子線程2中設置和訪問它的值
private void threadLocal() {
mThreadLocal.set(true);
Log.d(TAG, "[Thread#main]threadLocal=" + mThreadLocal.get());
new Thread() {
@Override
public void run() {
super.run();
mThreadLocal.set(false);
Log.d(TAG, "[Thread#1]threadLocal=" + mThreadLocal.get());
}
}.start();
new Thread() {
@Override
public void run() {
super.run();
Log.d(TAG, "[Thread#2]threadLocal=" + mThreadLocal.get());
}
}.start();
}
日志如下
從上面的日志可以看出砸抛,雖然在不同的線程中訪問的是同一個 ThreadLocal 對象评雌,但是通過 ThreadLocal 獲取到的值是不一樣的。ThreadLocal 之所以有這樣的效果直焙,是因為不同線程訪問同一個 ThreadLocal 的 get 方法景东,ThreadLocal 內部會從各自的線程中取出一個數組,然后在從數組中根據當前 ThreadLocal 的索引去查找出對應的 value 值奔誓,很顯然斤吐,不同線程中的數組是不同的,這就是為什么通過 ThreadLocal 可以在不同的線程中維護一套數據的副本并且彼此互不干擾厨喂。
ThreadLocal 是一個泛型類型和措,它的定義為 public class ThreadLocal<T>,只要弄清楚 ThreadLocal 的 get 和 set 方法就可以明白它的工作原理蜕煌。
首先看 ThreadLocal 的 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 {@link #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 = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
在上面的 set 方法中派阱,首先會獲取當前線程,通過 getMap(Thread t) 來獲取 ThreadLocalMap 斜纪,如果這個 map 不為空的話贫母,就將 ThreadLocal 和 我們想存放的 value 設置進去,不然的話就創(chuàng)建一個 ThreadLocalMap 然后再進行設置盒刚。
/**
* Get the map associated with a ThreadLocal. Overridden in
* InheritableThreadLocal.
*
* @param t the current thread
* @return the map
*/
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
/**
* Create the map associated with a ThreadLocal. Overridden in
* InheritableThreadLocal.
*
* @param t the current thread
* @param firstValue value for the initial entry of the map
*/
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
ThreadLocalMap 是 ThreadLocal 里面的靜態(tài)內部類腺劣,而每一個 Thread 都有一個對應的 ThreadLocalMap,所以 getMap 是直接返回 Thread 的成員因块,在 Thread 類中有一個成員專門用于存儲線程的 ThreadLocal 的數據如下所示
public class Thread implements Runnable {
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class.
*/
ThreadLocal.ThreadLocalMap threadLocals = null;
}
在 threadLocals 內部有一個數組:private Entry[] table橘原,ThreadLocal 的值就是存在這個 table 數組中
看下 ThreadLocal 的內部類 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 (i.e. entry.get()
* == null) 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<?>> {
/** The value associated with this ThreadLocal. */
Object value;
// 類似于map的key,value結構贮聂,key就是ThreadLocal靠柑,value就是需要隔離訪問的變量
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
/**
* The table, resized as necessary.
* table.length MUST always be a power of two.
*/
// 用數組保存了 Entry寨辩,因為可能有多個變量需要線程隔離訪問
private Entry[] table;
}
可以看到有個 Entry 內部靜態(tài)類吓懈,它繼承了 WeakReference,總之它記錄了兩個信息靡狞,一個是 ThreadLocal<?>類型耻警,一個是 Object 類型的值。getEntry 方法則是獲取某個 ThreadLocal 對應的值,set 方法就是更新或賦值相應的 ThreadLocal 對應的值甘穿。
下面看 threadLocals 是如何使用 set 方法將 ThreadLocal 的值存儲到 table 數組中的腮恩,如下所示
/**
* Set the value associated with key.
*
* @param key the thread local object
* @param value the value to be set
*/
private void set(ThreadLocal<?> key, Object value) {
// We don't use a fast path as with get() because it is at
// least as common to use set() to create new entries as
// it is to replace existing ones, in which case, a fast
// path would fail more often than not.
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
if (k == key) {
e.value = value;
return;
}
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
上面分析了ThreadLocal的set方法,這里分析下它的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 {@link #initialValue} method.
*
* @return the current thread's value of this thread-local
*/
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
回顧我們的 get 方法秸滴,其實就是拿到每個線程獨有的 ThreadLocalMap 然后再用 ThreadLocal 的當前實例,拿到 Map 中的相應的 Entry募判,然后就可以拿到相應的值返回出去荡含。當然,如果 Map 為空届垫,還會先進行 map 的創(chuàng)建释液,初 始化等工作。
ThreadLocal 的 get() 方法的邏輯也比較清晰装处,它同樣是取出當前線程的 threadLocals 對象误债,如果這個對象為 null,就調用 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 = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
在 setInitialValue() 方法中妄迁,將 initialValue() 的值賦給我們想要的值寝蹈,默認情況下,initialValue() 的值為 null登淘,當然也可以重寫這個方法躺盛。
protected T initialValue() {
return null;
}
從 ThreadLocal 的 set() 和 get() 方法可以看出,他們所操作的對象都是當前線程的 threalLocals 對象的 table 數組形帮,因此在不同的線程中訪問同一個 ThreadLocal 的 set() 和 get() 方法槽惫,他們對 ThreadLocal 所做的 讀 / 寫 操作權限僅限于各自線程的內部,這就是為什么可以在多個線程中互不干擾地存儲和修改數據辩撑。
總結
ThreadLocal 是線程內部的數據存儲類界斜,每個線程中都會保存一個ThreadLocal.ThreadLocalMap threadLocals = null;,ThreadLocalMap 是 ThreadLocal 的靜態(tài)內部類合冀,里面保存了一個 private Entry[] table 數組各薇,這個數組就是用來保存 ThreadLocal 中的值。通過這種方式君躺,就能讓我們在多個線程中互不干擾地存儲和修改數據峭判。
ThreadLocal 引發(fā)的內存泄漏分析
預備知識
引用 Object o = new Object();
這個 o,我們可以稱之為對象引用棕叫,而 new Object() 我們可以稱之為在內存中產生了一個對象實例林螃。
當寫下 o=null 時,只是表示 o 不再指向堆中 object 的對象實例俺泣,不代表這個對象實例不存在了疗认。
強引用就是指在程序代碼之中普遍存在的完残,類似“Object obj=new Object()” 這類的引用,只要強引用還存在横漏,垃圾收集器永遠不會回收掉被引用的對象實例谨设。
軟引用是用來描述一些還有用但并非必需的對象。對于軟引用關聯著的對象缎浇, 在系統(tǒng)將要發(fā)生內存溢出異常之前扎拣,將會把這些對象實例列進回收范圍之中進行第二次回收。如果這次回收還沒有足夠的內存素跺,才會拋出內存溢出異常鹏秋。在 JDK 1.2 之后,提供了 SoftReference 類來實現軟引用亡笑。
弱引用也是用來描述非必需對象的侣夷,但是它的強度比軟引用更弱一些,被弱 引用關聯的對象實例只能生存到下一次垃圾收集發(fā)生之前仑乌。當垃圾收集器工作時百拓, 無論當前內存是否足夠,都會回收掉只被弱引用關聯的對象實例晰甚。在 JDK 1.2 之 后衙传,提供了 WeakReference 類來實現弱引用。
虛引用也稱為幽靈引用或者幻影引用厕九,它是最弱的一種引用關系蓖捶。一個對象 實例是否有虛引用的存在,完全不會對其生存時間構成影響扁远,也無法通過虛引用 來取得一個對象實例俊鱼。為一個對象設置虛引用關聯的唯一目的就是能在這個對象 實例被收集器回收時收到一個系統(tǒng)通知。在 JDK 1.2 之后畅买,提供了 PhantomReference 類來實現虛引用并闲。
內存泄漏的現象
代碼示例:
/**
* 類說明:ThreadLocal造成的內存泄漏演示
*/
public class ThreadLocalOOM {
private static final int TASK_LOOP_SIZE = 500;
final static ThreadPoolExecutor poolExecutor
= new ThreadPoolExecutor(5, 5,
1,
TimeUnit.MINUTES,
new LinkedBlockingQueue<>());
static class LocalVariable {
private byte[] a = new byte[1024*1024*5];/*5M大小的數組*/
}
final static ThreadLocal<LocalVariable> localVariable
= new ThreadLocal<>();
public static void main(String[] args) throws InterruptedException {
Object o = new Object();
/*5*5=25*/
for (int i = 0; i < TASK_LOOP_SIZE; ++i) {
poolExecutor.execute(new Runnable() {
public void run() {
//localVariable.set(new LocalVariable());
new LocalVariable();
System.out.println("use local varaible");
//localVariable.remove();
}
});
Thread.sleep(100);
}
System.out.println("pool execute over");
}
}
執(zhí)行上面 ThreadLocalOOM,并將堆內存大小設 置為-Xmx256m谷羞,我們啟用一個線程池著觉,大小固定為 5 個線程
首先只簡單的在每個任務中 new 出一個數組
可以看到內存的實際使用控制在 25M 左右:因為每個任務中會不斷 new 出 一個 5M 的數組炊豪,5*5=25M陨亡,這是很合理的舵稠。
當我們啟用了 ThreadLocal 以后:
內存占用最高升至 150M,一般情況下穩(wěn)定在 90M 左右嗓违,那么加入一個 ThreadLocal 后九巡,內存的占用真的會這么多?
于是靠瞎,我們加入一行代碼:
再執(zhí)行比庄,看看內存情況:
可以看見最高峰的內存占用也在 25M 左右求妹,完全和我們不加 ThreadLocal 表現一樣乏盐。
這就充分說明佳窑,確實發(fā)生了內存泄漏。
分析
根據我們前面對 ThreadLocal 的分析父能,我們可以知道每個 Thread 維護一個 ThreadLocalMap神凑,這個映射表的 key 是 ThreadLocal 實例本身,value 是真正需 要存儲的 Object何吝,也就是說 ThreadLocal 本身并不存儲值溉委,它只是作為一個 key 來讓線程從 ThreadLocalMap 獲取 value。仔細觀察 ThreadLocalMap爱榕,這個 map 是使用 ThreadLocal 的弱引用作為 Key 的瓣喊,弱引用的對象在 GC 時會被回收。
因此使用了 ThreadLocal 后黔酥,引用鏈如圖所示
圖中的虛線表示弱引用藻三。
這樣,當把 ThreadLocal 變量置為 null 以后跪者,沒有任何強引用指向 ThreadLocal 實例棵帽,所以 ThreadLocal 將會被 GC 回收。這樣一來渣玲,ThreadLocalMap 中就會出現 key 為 null 的 Entry逗概,就沒有辦法訪問這些 key 為 null 的 Entry 的 value,如果當前線程再遲遲不結束的話忘衍,這些 key 為 null 的 Entry 的 value 就會一直存在一條強 引用鏈:Thread Ref -> Thread -> ThreadLocalMap -> Entry -> value逾苫,而這塊 value 永遠不會被訪問到了,所以存在著內存泄露枚钓。
只有當前 thread 結束以后隶垮,current thread 就不會存在棧中,強引用斷開秘噪, Current Thread狸吞、Map value 將全部被 GC 回收。最好的做法是不在需要使用 ThreadLocal 變量后指煎,都調用它的 remove()方法蹋偏,清除數據。
其實考察 ThreadLocal 的實現至壤,我們可以看見威始,無論是 get()、set()在某些時 候像街,調用了 expungeStaleEntry
方法用來清除 Entry 中 Key 為 null 的 Value黎棠,但是這是不及時的晋渺,也不是每次都會執(zhí)行的,所以一些情況下還是會發(fā)生內存泄露脓斩。 只有 remove() 方法中顯式調用了 expungeStaleEntry 方法木西。
從表面上看內存泄漏的根源在于使用了弱引用,但是另一個問題也同樣值得 思考:為什么使用弱引用而不是強引用随静?
下面我們分兩種情況討論:
key 使用強引用:引用 ThreadLocal 的對象被回收了八千,但是 ThreadLocalMap 還持有 ThreadLocal 的強引用,如果沒有手動刪除燎猛,ThreadLocal 的對象實例不會被回收恋捆,導致 Entry 內存泄漏。
key 使用弱引用:引用的 ThreadLocal 的對象被回收了重绷,由于 ThreadLocalMap 持有 ThreadLocal 的弱引用沸停,即使沒有手動刪除,ThreadLocal 的對象實例也會被回收昭卓。value 在下一次 ThreadLocalMap 調用 set愤钾,get,remove 都有機會被回收葬凳。
比較兩種情況绰垂,我們可以發(fā)現:由于 ThreadLocalMap 的生命周期跟 Thread 一樣長,如果都沒有手動刪除對應 key火焰,都會導致內存泄漏劲装,但是使用弱引用可 以多一層保障。
因此昌简,ThreadLocal 內存泄漏的根源是:由于 ThreadLocalMap 的生命周期跟 Thread 一樣長占业,如果沒有手動刪除對應 key 就會導致內存泄漏,而不是因為弱引用纯赎。
總結
- JVM 利用設置 ThreadLocalMap 的 Key 為弱引用谦疾,來避免內存泄露。
- JVM 利用調用 remove犬金、get念恍、set 方法的時候,回收弱引用晚顷。
- 當 ThreadLocal 存儲很多 Key 為 null 的 Entry 的時候峰伙,而不再去調用 remove、 get该默、set 方法瞳氓,那么將導致內存泄漏。
- 使用線程池+ ThreadLocal 時要小心栓袖,因為這種情況下匣摘,線程是一直在不斷的重復運行的店诗,從而也就造成了 value 可能造成累積的情況。
錯誤使用 ThreadLocal 導致線程不安全
/**
* 類說明:ThreadLocal的線程不安全演示
*/
public class ThreadLocalUnsafe implements Runnable {
public static Number number = new Number(0);
public void run() {
//每個線程計數加一
number.setNum(number.getNum() + 1);
//將其存儲到ThreadLocal中
value.set(number);
SleepTools.ms(2);
//輸出num值
System.out.println(Thread.currentThread().getName() + "=" + value.get().getNum());
}
public static ThreadLocal<Number> value = new ThreadLocal<Number>() {
};
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
new Thread(new ThreadLocalUnsafe()).start();
}
}
private static class Number {
public Number(int num) {
this.num = num;
}
private int num;
public int getNum() {
return num;
}
public void setNum(int num) {
this.num = num;
}
@Override
public String toString() {
return "Number [num=" + num + "]";
}
}
}
運行結果:
為什么每個線程都輸出 5音榜?難道他們沒有獨自保存自己的 Number 副本嗎庞瘸? 為什么其他線程還是能夠修改這個值?仔細考察 ThreadLocal 和 Thead 的代碼囊咏, 我們發(fā)現 ThreadLocalMap 中保存的其實是對象的一個引用恕洲,這樣的話塔橡,當有其 他線程對這個引用指向的對象實例做修改時梅割,其實也同時影響了所有的線程持有 的對象引用所指向的同一個對象實例。這也就是為什么上面的程序為什么會輸出一樣的結果:5 個線程中保存的是同一 Number 對象的引用葛家,在線程睡眠的時候户辞, 其他線程將 num 變量進行了修改,而修改的對象 Number 的實例是同一份癞谒,因此它們最終輸出的結果是相同的底燎。
而上面的程序要正常的工作,應該的用法是讓每個線程中的 ThreadLocal 都應該持有一個新的 Number 對象弹砚。去掉 public static Number number = new Number(0);中的 static 即可正常工作双仍。