本片文章的主要內容如下:
- 1、Java中的ThreadLocal
- 2沉唠、 Android中的ThreadLocal
- 3、Android 面試中的關于ThreadLocal的問題
- 4仗考、ThreadLocal的總結
Java中的ThreadLocal和Android中的ThreadLocal的源代碼是不一樣的
- 注:基于Android 6.0(6.0.1_r10)/API 23 源碼
使用
public class ThreadLocalTest {
private static ThreadLocal<String> sLocal = new ThreadLocal<>();
private static ThreadLocal<Integer> sLocal2 = new ThreadLocal<>();
public static void main(String[] args) {
List<Thread> threads = new ArrayList<>();
for (int i = 0; i < 5; i++) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
String threadName = Thread.currentThread().getName();
System.out.println("thread name " + threadName);
sLocal.set(threadName);
sLocal2.set(Integer.parseInt(threadName) + 10);
try {
Thread.sleep(500 * Integer.parseInt(threadName));
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("thread name " + threadName + " ---local " + sLocal.get() + " ---local " + sLocal2.get());
}
});
threads.add(thread);
thread.setName(i + "");
thread.start();
}
}
}
1 Java中的ThreadLocal
1.1 ThreadLocal的前世今生
- 早在JDK1.2的版本中就提供java.lang.ThreadLocal翎嫡,ThreadLocal為解決多線程程序的并發(fā)問題提供了一種新的思路唠雕,并在JDK1.5開始支持泛型。
- 當使用ThreadLocal維護變量時骇吭,ThreadLocal為每個使用該變量的線程提供獨立的變量副本橙弱,所以每一個線程都可以獨立的改變自己的副本,而不會影響其他線程所對應的副本燥狰。
- 從線程的角度來看棘脐,目標變量就像是本地變量,這也是類名中"Local"所要表達的意思龙致。所以蛀缝,在Java中編寫線程局部變量的代碼相對來說要"笨拙"一些,因此造成了線程局部變量沒有在Java開發(fā)者得到很好的普及目代。
1.2 ThreadLocal類簡介
1.2.1 Java源碼描述
ThreadLocal類用來提供線程內部的局部變量屈梁,這種變量在多線程環(huán)境下訪問(通過get或set方法訪問)時能保證各個線程里的變量相對獨立于其他線程內的變量。ThreadLocal實例通常來說都是private static 類型榛了,用于關聯線程在讶。
ThreadLocal設計的初衷:提供線程內部的局部變量,在本地線程內隨時隨地可取霜大,隔離其他線程构哺。
ThreadLocal的作用是提供線程內的局部變量,這種局部變量僅僅在線程的生命周期中起作用战坤,減少同一個線程內多個函數或者組件一些公共變量的傳遞的復雜度曙强。
1.2.2 ThreadLocal類結構
ThreadLocal的結構圖:
1.2.3 ThreadLocal常用的方法
1.2.3.1 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);
}
代碼流程很清晰:
- 先拿到保存鍵值對的ThreadLocalMap對象實例map,如果map為空(即第一次調用的時候map值為null)碟嘴,則去創(chuàng)建一個ThreadLocalMap對象并賦值給map慈省,并把鍵值對保存在map
- 我們看到首先是拿到當前先線程實例t眠菇,任何將t作為參數構造ThreadLocalMap對象袱衷,為什么需要通過Threadl來獲取ThreadLocalMap對象笑窜?
//ThreadLocal.java
/**
* 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;
}
- 我們看到getMap實現非常直接,就是直接返回Thread對象的threadLocal字段排截。Thread類中的ThreadLocalMap字段聲明如下:
//Thread.java
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
我們總結一下:
- ThreadLocal的set(T) 方法中,首先是拿到當前線程Thread對象中的ThreadLocalMap對象實例threadLocals脱吱,然后再將需要保存的值保存到threadLocals里面。
- 每個線程引用的ThreadLocal副本值都是保存在當前Thread對象里面的认罩。存儲結構為ThreadLocalMap類型箱蝠,ThreadLocalMap保存的類型為ThreadLocal,值為副本值
1.2.3.2 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)
return (T)e.value;
}
return setInitialValue();
}
- 同樣的道理垦垂,拿到當前線程Thread對象實例中保存的ThreadLocalMap對象map宦搬,然后從map中讀取鍵為this(即ThreadLocal類實例)對應的值。
- 如果map不是null劫拗,直接從map里面讀取就好了间校,如果map==null,那么我們需要對當前線程Thread對象實例中保存ThreadLocalMap對象new一下页慷。即通過setInitialValue()方法來創(chuàng)建憔足,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;
}
代碼很清晰,通過createMap來創(chuàng)建ThreadLocalMap對象酒繁,前面set(T)方法里面的ThreadLocalMap也是通過createMap來的滓彰,我們看看createMap具體實現:
/**
* 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
* @param map the map to store.
*/
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
1.2.3.3
remove()方法
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
- 直接將當前線程局部變量的值刪除,目的是為了減少內存的占用欲逃,該方法是JDK1.5新增的方法找蜜。
- 需要指出的,當線程結束以后稳析,對該應線程的局部變量將自動被垃圾回收洗做,所以顯示調用該方法清除線程的局部變量并不是必須的操作,但是它可以加速內存回收的數據彰居。
1.3 內部類ThreadLocalMap
/**
* ThreadLocalMap is a customized hash map suitable only for
* maintaining thread local values. No operations are exported
* outside of the ThreadLocal class. The class is package private to
* allow declaration of fields in class Thread. To help deal with
* very large and long-lived usages, the hash table entries use
* WeakReferences for keys. However, since reference queues are not
* used, stale entries are guaranteed to be removed only when
* the table starts running out of space.
*/
static class ThreadLocalMap {}
- ThreadLocalMap是一個適用于維護線程本地值的自定義哈希映射(hash map)诚纸,沒有任何操作可以讓它超出ThreadLocal這個類的范圍。
- 該類是私有的陈惰,允許在Thread類中聲明字段畦徘。為了更好的幫助處理常使用的,hash表條目使用了WeakReferences的鍵。但是井辆,由于不使用引用隊列关筒,所以杯缺,只有在表空間不足的情況下萍肆,才會保留已經刪除的條目
1.3.1 存儲結構
- 通過注釋我們知道ThreadLocalMap中存儲的是ThreadLocalMap.Entry(后面用Entry代替)對象塘揣。
- 在ThreadLocalMap中管理的也就是Entry對象才写。
- 首先ThreadlocalMap需要一個"容器"來存儲這些Entry對象琅摩,ThreadLocalMap中定義了額Entry數據實例table房资,用于存儲Entry
/**
* The table, resized as necessary.
* table.length MUST always be a power of two.
*/
private Entry[] table;
- ThreadLocalMap維護一張哈希表(一個數組)轰异,表里存儲Entry搭独。既然是哈希表牙肝,那肯定會涉及到加載因子配椭,即當表里面存儲的對象達到容量的多少百分比的時候需要擴容股缸。
- ThreadLocalMap中定義了threshold屬性瘾境,當表里存儲的對象數量超過了threshold就會擴容迷守。
/**
* The next size value at which to resize.
*/
private int threshold; // Default to 0
/**
* Set the resize threshold to maintain at worst a 2/3 load factor.
*/
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
從上面代碼可以看出盒犹,加載因子設置為2/3。即每次容量超過設定的len2/3時沮协,需要擴容聘殖。
1.3.2 存儲Entry對象
- hash散列的數據在存儲過程中可能會發(fā)生碰撞奸腺,大家知道HashMap存儲的是一個Entry鏈突照,當hash發(fā)生沖突后氧吐,將新的的Entry存放在鏈表的最前端筑舅。但是ThreadLocalMap不一樣翠拣,采用的是*index+1作為重散列的hash值寫入误墓。
- 另外有一點需要注意key出現null的原因由于Entry的key是繼承了弱引用优烧,在下一次GC是不管它有沒有被引用都會被回收畦娄。
- 當出現null時,會調用replaceStaleEntry()方法循環(huán)尋找相同的key励饵,如果存在滑燃,直接替換舊值典予。如果不存在瘤袖,則在當前位置上重新創(chuàng)新的Entry昂验。
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
看下代碼:
/**
* 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;
}
//和HashMap不一樣,因為Entry key 繼承了所引用甫恩,所以會出現key是null的情況填物!所以會接著在replaceStaleEntry()重新循環(huán)尋找相同的key
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
/**
* Increment i modulo len.
*/
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
- 通過key(ThreadLocal類型)的hashcode來計算存儲的索引位置 i 升薯。
- 如果 i 位置已經存儲了對象涎劈,那么就向后挪一個位置以此類推蛛枚,直到找到空的位置蹦浦,再講對象存放盲镶。
- 在最后還需要判斷一下當前的存儲的對象個數是否已經超出了反之(threshold的值)大小,如果超出了枫吧,需要重新擴充并將所有的對象重新計算位置(rehash函數來實現)九杂。
1.3.2.1 rehash()方法
/**
* Re-pack and/or re-size the table. First scan the entire
* table removing stale entries. If this doesn't sufficiently
* shrink the size of the table, double the table size.
*/
private void rehash() {
expungeStaleEntries();
// Use lower threshold for doubling to avoid hysteresis
if (size >= threshold - threshold / 4)
resize();
}
rehash函數里面先是調用了expungeStaleEntries()函數,然后再判斷當前存儲對象的小時是否超出了閥值的3/4裳擎。如果超出了思币,再擴容谷饿。
ThreadLocalMap里面存儲的Entry對象本質上是一個WeakReference<ThreadLcoal>博投。也就是說毅哗,ThreadLocalMap里面存儲的對象本質是一個隊ThreadLocal的弱引用虑绵,該ThreadLocal隨時可能會被回收!即導致ThreadLocalMap里面對應的 value的Key是null捕发。我們需要把這樣的Entry清除掉很魂,不要讓他們占坑法挨。
expungeStaleEntries函數就是做這樣的清理工作坷剧,清理完后惫企,實際存儲的對象數量自然會減少丛版,這也不難理解后面的判斷的約束條件為閥值的3/4页畦,而不是閥值的大小豫缨。
1.3.2.2 expungeStaleEntries()與expungeStaleEntry()方法
/**
* Expunge all stale entries in the table.
*/
private void expungeStaleEntries() {
Entry[] tab = table;
int len = tab.length;
for (int j = 0; j < len; j++) {
Entry e = tab[j];
if (e != null && e.get() == null)
expungeStaleEntry(j);
}
}
expungeStaleEntries()方法很簡單好芭,主要是遍歷table,然后調用expungeStaleEntry()敬拓,下面我們來主要講解下這個函數expungeStaleEntry()函數乘凸。
1.3.2.3 expungeStaleEntry()方法
ThreadLocalMap中的expungeStaleEntry(int)方法的可能被調用的處理有:
通過上面的圖木人,不難看出醒第,這個方法在ThreadLocal的set稠曼、get霞幅、remove時都會被調用。
/**
* Expunge a stale entry by rehashing any possibly colliding entries
* lying between staleSlot and the next null slot. This also expunges
* any other stale entries encountered before the trailing null. See
* Knuth, Section 6.4
*
* @param staleSlot index of slot known to have null key
* @return the index of the next null slot after staleSlot
* (all between staleSlot and this slot will have been checked
* for expunging).
*/
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// expunge entry at staleSlot
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
// Rehash until we encounter null
Entry e;
int i;
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal k = e.get();
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else {
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
tab[i] = null;
// Unlike Knuth 6.4 Algorithm R, we must scan until
// null because multiple entries could have been stale.
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}
- 看出先清理指定的Entry,再遍歷,如果發(fā)現有Entry的key為null试读,就清理钩骇。
- Key==null,也就是ThreadLocal對象是null铝量。所以當程序中倘屹,將ThreadLocal對象設置為null,在該線程繼續(xù)執(zhí)行時款违,如果執(zhí)行另一個ThreadLocal時唐瀑,就會觸發(fā)該方法。就有可能清理掉Key是null的那個ThreadLocal對應的值插爹。
- 所以說expungStaleEntry()方法清除線程ThreadLocalMap里面所有key為null的value。
1.3.3 獲取Entry對象getEntry()
/**
* Get the entry associated with key. This method
* itself handles only the fast path: a direct hit of existing
* key. It otherwise relays to getEntryAfterMiss. This is
* designed to maximize performance for direct hits, in part
* by making this method readily inlinable.
*
* @param key the thread local object
* @return the entry associated with key, or null if no such
*/
private Entry getEntry(ThreadLocal key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);
}
- getEntry()方法很簡單请梢,直接通過哈希碼計算位置 i 赠尾,然后把哈希表對應的 i 的位置Entry對象拿出來够坐。
- 如果對應位置的值為null,這就存在如下幾種可能。
- key 對應的值為null
- 由于位置沖突七扰,key對應的值存儲的位置并不是 i 位置上立由,即 i 位置上的null并不屬于 key 值
因此枣耀,需要一個函數去確認key對應的value的值颅围,即getEntryAfterMiss()方法
1.3.3.1 getEntryAfterMiss()函數
/**
* Version of getEntry method for use when key is not found in
* its direct hash slot.
*
* @param key the thread local object
* @param i the table index for key's hash code
* @param e the entry at table[i]
* @return the entry associated with key, or null if no such
*/
private Entry getEntryAfterMiss(ThreadLocal key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
while (e != null) {
ThreadLocal k = e.get();
if (k == key)
return e;
if (k == null)
expungeStaleEntry(i);
else
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
- 第一步弄抬,從ThreadLocal的直接索引位置(通過ThreadLocal.threadLocalHashCode&(len-1)運算得到)獲取Entry e懊亡,如果e不為null坏瞄,并且key相同則返回e。
- 第二步,如果e為null或者key不一致則向下一個位置查詢弱匪,如果下一個位置的key和當前需要查詢的key相等帘饶,則返回應對應的Entry淹禾,否則智嚷,如果key值為null衅枫,則擦除該位置的Entry猾漫,否則繼續(xù)向一個位置查詢。
ThreadLocalMap整個get過程中遇到的key為null的Entry都被會擦除,那么value的上一個引用鏈就不存在了,自然會被回收掏膏。set也有類似的操作马胧。這樣在你每次調用ThreadLocal的get方法去獲取值或者調用set方法去設置值的時候崔列,都會去做這個操作组底,防止內存泄露,當然最保險的還是通過手動調用remove方法直接移除
1.3.4 ThreadLocalMap.Entry對象
前面很多地方都在說ThreadLocalMap里面存儲的是ThreadLocalMap.Entry對象懂牧,那么ThreadLocalMap.Entry獨享到底是如何存儲鍵值對的?同時有是如何做到的對ThreadLocal對象進行弱引用?
/**
* 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;
Entry(ThreadLocal k, Object v) {
super(k);
value = v;
}
}
- 從源碼的繼承關系可以看到,Entry是繼承WeakReference<ThreadLocal>鲁森。即Entry本質上就是WeakReference<ThreadLocal>```
-
Entry就是一個弱引用痛垛,具體講碟婆,Entry實例就是對ThreadLocal某個實例的弱引用。只不過唇撬,Entry同時還保存了value
1.4 總結
- ThreadLocal是解決線程安全的一個很好的思路它匕,它通過為每個線程提供了一個獨立的變量副本解決了額變量并發(fā)訪問的沖突問題。
- 在很多情況下窖认,ThreadLocal比直接使用synchronized同步機制解決線程安全問題更簡單豫柬,更方便告希,且結果程序擁有更高的并發(fā)性。
- ThreadLocal和synchronize用一句話總結就是一個用存儲拷貝進行空間換時間烧给,一個是用鎖機制進行時間換空間燕偶。
其實補充知識: - ThreadLocal官方建議已定義成private static 的這樣讓Thread不那么容易被回收
- 真正涉及共享變量的時候ThreadLocal是解決不了的。它最多是當每個線程都需要這個實例础嫡,如一個打開數據庫的對象時指么,保證每個線程拿到一個進而去操作,互不不影響榴鼎。但是這個對象其實并不是共享的汽馋。
4 ThreadLocal會存在內存泄露嗎
ThreadLocal實例的弱引用,當前thread被銷毀時归薛,ThreadLocal也會隨著銷毀被GC回收。