本片文章的主要內(nèi)容如下:
- 1、Java中的ThreadLocal
- 2、 Android中的ThreadLocal
- 3、Android 面試中的關(guān)于ThreadLocal的問(wèn)題
- 4、ThreadLocal的總結(jié)
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ā)問(wèn)題提供了一種新的思路,并在JDK1.5開(kāi)始支持泛型。
- 當(dāng)使用ThreadLocal維護(hù)變量時(shí),ThreadLocal為每個(gè)使用該變量的線程提供獨(dú)立的變量副本,所以每一個(gè)線程都可以獨(dú)立的改變自己的副本,而不會(huì)影響其他線程所對(duì)應(yīng)的副本。
- 從線程的角度來(lái)看,目標(biāo)變量就像是本地變量,這也是類名中"Local"所要表達(dá)的意思。所以,在Java中編寫線程局部變量的代碼相對(duì)來(lái)說(shuō)要"笨拙"一些,因此造成了線程局部變量沒(méi)有在Java開(kāi)發(fā)者得到很好的普及。
1.2 ThreadLocal類簡(jiǎn)介
1.2.1 Java源碼描述
ThreadLocal類用來(lái)提供線程內(nèi)部的局部變量,這種變量在多線程環(huán)境下訪問(wèn)(通過(guò)get或set方法訪問(wèn))時(shí)能保證各個(gè)線程里的變量相對(duì)獨(dú)立于其他線程內(nèi)的變量。ThreadLocal實(shí)例通常來(lái)說(shuō)都是private static 類型,用于關(guān)聯(lián)線程。
ThreadLocal設(shè)計(jì)的初衷:提供線程內(nèi)部的局部變量,在本地線程內(nèi)隨時(shí)隨地可取,隔離其他線程。
ThreadLocal的作用是提供線程內(nèi)的局部變量,這種局部變量?jī)H僅在線程的生命周期中起作用,減少同一個(gè)線程內(nèi)多個(gè)函數(shù)或者組件一些公共變量的傳遞的復(fù)雜度。
1.2.2 ThreadLocal類結(jié)構(gòu)
ThreadLocal的結(jié)構(gòu)圖:
1.2.3 ThreadLocal常用的方法
1.2.3.1 set方法
設(shè)置當(dāng)前線程的線程局部變量的值
/**
* 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);
}
代碼流程很清晰:
- 先拿到保存鍵值對(duì)的ThreadLocalMap對(duì)象實(shí)例map,如果map為空(即第一次調(diào)用的時(shí)候map值為null),則去創(chuàng)建一個(gè)ThreadLocalMap對(duì)象并賦值給map,并把鍵值對(duì)保存在map
- 我們看到首先是拿到當(dāng)前先線程實(shí)例t,任何將t作為參數(shù)構(gòu)造ThreadLocalMap對(duì)象,為什么需要通過(guò)Threadl來(lái)獲取ThreadLocalMap對(duì)象?
//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實(shí)現(xiàn)非常直接,就是直接返回Thread對(duì)象的threadLocal字段。Thread類中的ThreadLocalMap字段聲明如下:
//Thread.java
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
我們總結(jié)一下:
- ThreadLocal的set(T) 方法中,首先是拿到當(dāng)前線程Thread對(duì)象中的ThreadLocalMap對(duì)象實(shí)例threadLocals,然后再將需要保存的值保存到threadLocals里面。
- 每個(gè)線程引用的ThreadLocal副本值都是保存在當(dāng)前Thread對(duì)象里面的。存儲(chǔ)結(jié)構(gòu)為ThreadLocalMap類型,ThreadLocalMap保存的類型為ThreadLocal,值為副本值
1.2.3.2 get方法
該方法返回當(dāng)前線程所對(duì)應(yīng)的線程局部變量
/**
* 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();
}
- 同樣的道理,拿到當(dāng)前線程Thread對(duì)象實(shí)例中保存的ThreadLocalMap對(duì)象map,然后從map中讀取鍵為this(即ThreadLocal類實(shí)例)對(duì)應(yīng)的值。
- 如果map不是null,直接從map里面讀取就好了,如果map==null,那么我們需要對(duì)當(dāng)前線程Thread對(duì)象實(shí)例中保存ThreadLocalMap對(duì)象new一下。即通過(guò)setInitialValue()方法來(lái)創(chuàng)建,setInitialValue()方法的具體實(shí)現(xiàn)如下:
/**
* 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;
}
代碼很清晰,通過(guò)createMap來(lái)創(chuàng)建ThreadLocalMap對(duì)象,前面set(T)方法里面的ThreadLocalMap也是通過(guò)createMap來(lái)的,我們看看createMap具體實(shí)現(xiàn):
/**
* 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);
- 直接將當(dāng)前線程局部變量的值刪除,目的是為了減少內(nèi)存的占用,該方法是JDK1.5新增的方法。
- 需要指出的,當(dāng)線程結(jié)束以后,對(duì)該應(yīng)線程的局部變量將自動(dòng)被垃圾回收,所以顯示調(diào)用該方法清除線程的局部變量并不是必須的操作,但是它可以加速內(nèi)存回收的數(shù)據(jù)。
1.3 內(nèi)部類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是一個(gè)適用于維護(hù)線程本地值的自定義哈希映射(hash map),沒(méi)有任何操作可以讓它超出ThreadLocal這個(gè)類的范圍。
- 該類是私有的,允許在Thread類中聲明字段。為了更好的幫助處理常使用的,hash表?xiàng)l目使用了WeakReferences的鍵。但是,由于不使用引用隊(duì)列,所以,只有在表空間不足的情況下,才會(huì)保留已經(jīng)刪除的條目
1.3.1 存儲(chǔ)結(jié)構(gòu)
- 通過(guò)注釋我們知道ThreadLocalMap中存儲(chǔ)的是ThreadLocalMap.Entry(后面用Entry代替)對(duì)象。
- 在ThreadLocalMap中管理的也就是Entry對(duì)象。
- 首先ThreadlocalMap需要一個(gè)"容器"來(lái)存儲(chǔ)這些Entry對(duì)象,ThreadLocalMap中定義了額Entry數(shù)據(jù)實(shí)例table,用于存儲(chǔ)Entry
/**
* The table, resized as necessary.
* table.length MUST always be a power of two.
*/
private Entry[] table;
- ThreadLocalMap維護(hù)一張哈希表(一個(gè)數(shù)組),表里存儲(chǔ)Entry。既然是哈希表,那肯定會(huì)涉及到加載因子,即當(dāng)表里面存儲(chǔ)的對(duì)象達(dá)到容量的多少百分比的時(shí)候需要擴(kuò)容。
- ThreadLocalMap中定義了threshold屬性,當(dāng)表里存儲(chǔ)的對(duì)象數(shù)量超過(guò)了threshold就會(huì)擴(kuò)容。
/**
* 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;
}
從上面代碼可以看出,加載因子設(shè)置為2/3。即每次容量超過(guò)設(shè)定的len2/3時(shí),需要擴(kuò)容。
1.3.2 存儲(chǔ)Entry對(duì)象
- hash散列的數(shù)據(jù)在存儲(chǔ)過(guò)程中可能會(huì)發(fā)生碰撞,大家知道HashMap存儲(chǔ)的是一個(gè)Entry鏈,當(dāng)hash發(fā)生沖突后,將新的的Entry存放在鏈表的最前端。但是ThreadLocalMap不一樣,采用的是*index+1作為重散列的hash值寫入。
- 另外有一點(diǎn)需要注意key出現(xiàn)null的原因由于Entry的key是繼承了弱引用,在下一次GC是不管它有沒(méi)有被引用都會(huì)被回收。
- 當(dāng)出現(xiàn)null時(shí),會(huì)調(diào)用replaceStaleEntry()方法循環(huán)尋找相同的key,如果存在,直接替換舊值。如果不存在,則在當(dāng)前位置上重新創(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
*/
//設(shè)置當(dāng)前線程的線程局部變量的值
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不一樣,因?yàn)镋ntry key 繼承了所引用,所以會(huì)出現(xiàn)key是null的情況!所以會(huì)接著在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);
}
- 通過(guò)key(ThreadLocal類型)的hashcode來(lái)計(jì)算存儲(chǔ)的索引位置 i 。
- 如果 i 位置已經(jīng)存儲(chǔ)了對(duì)象,那么就向后挪一個(gè)位置以此類推,直到找到空的位置,再講對(duì)象存放。
- 在最后還需要判斷一下當(dāng)前的存儲(chǔ)的對(duì)象個(gè)數(shù)是否已經(jīng)超出了反之(threshold的值)大小,如果超出了,需要重新擴(kuò)充并將所有的對(duì)象重新計(jì)算位置(rehash函數(shù)來(lái)實(shí)現(xiàn))。
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函數(shù)里面先是調(diào)用了expungeStaleEntries()函數(shù),然后再判斷當(dāng)前存儲(chǔ)對(duì)象的小時(shí)是否超出了閥值的3/4。如果超出了,再擴(kuò)容。
ThreadLocalMap里面存儲(chǔ)的Entry對(duì)象本質(zhì)上是一個(gè)WeakReference<ThreadLcoal>。也就是說(shuō),ThreadLocalMap里面存儲(chǔ)的對(duì)象本質(zhì)是一個(gè)隊(duì)ThreadLocal的弱引用,該ThreadLocal隨時(shí)可能會(huì)被回收!即導(dǎo)致ThreadLocalMap里面對(duì)應(yīng)的 value的Key是null。我們需要把這樣的Entry清除掉,不要讓他們占坑。
expungeStaleEntries函數(shù)就是做這樣的清理工作,清理完后,實(shí)際存儲(chǔ)的對(duì)象數(shù)量自然會(huì)減少,這也不難理解后面的判斷的約束條件為閥值的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()方法很簡(jiǎn)單,主要是遍歷table,然后調(diào)用expungeStaleEntry(),下面我們來(lái)主要講解下這個(gè)函數(shù)expungeStaleEntry()函數(shù)。
1.3.2.3 expungeStaleEntry()方法
ThreadLocalMap中的expungeStaleEntry(int)方法的可能被調(diào)用的處理有:
通過(guò)上面的圖,不難看出,這個(gè)方法在ThreadLocal的set、get、remove時(shí)都會(huì)被調(diào)用。
/**
* 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ā)現(xiàn)有Entry的key為null,就清理。
- Key==null,也就是ThreadLocal對(duì)象是null。所以當(dāng)程序中,將ThreadLocal對(duì)象設(shè)置為null,在該線程繼續(xù)執(zhí)行時(shí),如果執(zhí)行另一個(gè)ThreadLocal時(shí),就會(huì)觸發(fā)該方法。就有可能清理掉Key是null的那個(gè)ThreadLocal對(duì)應(yīng)的值。
- 所以說(shuō)expungStaleEntry()方法清除線程ThreadLocalMap里面所有key為null的value。
1.3.3 獲取Entry對(duì)象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()方法很簡(jiǎn)單,直接通過(guò)哈希碼計(jì)算位置 i ,然后把哈希表對(duì)應(yīng)的 i 的位置Entry對(duì)象拿出來(lái)。
- 如果對(duì)應(yīng)位置的值為null,這就存在如下幾種可能。
- key 對(duì)應(yīng)的值為null
- 由于位置沖突,key對(duì)應(yīng)的值存儲(chǔ)的位置并不是 i 位置上,即 i 位置上的null并不屬于 key 值
因此,需要一個(gè)函數(shù)去確認(rèn)key對(duì)應(yīng)的value的值,即getEntryAfterMiss()方法
1.3.3.1 getEntryAfterMiss()函數(shù)
/**
* 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的直接索引位置(通過(guò)ThreadLocal.threadLocalHashCode&(len-1)運(yùn)算得到)獲取Entry e,如果e不為null,并且key相同則返回e。
- 第二步,如果e為null或者key不一致則向下一個(gè)位置查詢,如果下一個(gè)位置的key和當(dāng)前需要查詢的key相等,則返回應(yīng)對(duì)應(yīng)的Entry,否則,如果key值為null,則擦除該位置的Entry,否則繼續(xù)向一個(gè)位置查詢。
ThreadLocalMap整個(gè)get過(guò)程中遇到的key為null的Entry都被會(huì)擦除,那么value的上一個(gè)引用鏈就不存在了,自然會(huì)被回收。set也有類似的操作。這樣在你每次調(diào)用ThreadLocal的get方法去獲取值或者調(diào)用set方法去設(shè)置值的時(shí)候,都會(huì)去做這個(gè)操作,防止內(nèi)存泄露,當(dāng)然最保險(xiǎn)的還是通過(guò)手動(dòng)調(diào)用remove方法直接移除
1.3.4 ThreadLocalMap.Entry對(duì)象
前面很多地方都在說(shuō)ThreadLocalMap里面存儲(chǔ)的是ThreadLocalMap.Entry對(duì)象,那么ThreadLocalMap.Entry獨(dú)享到底是如何存儲(chǔ)鍵值對(duì)的?同時(shí)有是如何做到的對(duì)ThreadLocal對(duì)象進(jìn)行弱引用?
/**
* 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;
}
}
- 從源碼的繼承關(guān)系可以看到,Entry是繼承WeakReference<ThreadLocal>。即Entry本質(zhì)上就是WeakReference<ThreadLocal>```
-
Entry就是一個(gè)弱引用,具體講,Entry實(shí)例就是對(duì)ThreadLocal某個(gè)實(shí)例的弱引用。只不過(guò),Entry同時(shí)還保存了value
5713484-bc893ac35da6fd7b.png
1.4 總結(jié)
- ThreadLocal是解決線程安全的一個(gè)很好的思路,它通過(guò)為每個(gè)線程提供了一個(gè)獨(dú)立的變量副本解決了額變量并發(fā)訪問(wèn)的沖突問(wèn)題。
- 在很多情況下,ThreadLocal比直接使用synchronized同步機(jī)制解決線程安全問(wèn)題更簡(jiǎn)單,更方便,且結(jié)果程序擁有更高的并發(fā)性。
- ThreadLocal和synchronize用一句話總結(jié)就是一個(gè)用存儲(chǔ)拷貝進(jìn)行空間換時(shí)間,一個(gè)是用鎖機(jī)制進(jìn)行時(shí)間換空間。
其實(shí)補(bǔ)充知識(shí): - ThreadLocal官方建議已定義成private static 的這樣讓Thread不那么容易被回收
- 真正涉及共享變量的時(shí)候ThreadLocal是解決不了的。它最多是當(dāng)每個(gè)線程都需要這個(gè)實(shí)例,如一個(gè)打開(kāi)數(shù)據(jù)庫(kù)的對(duì)象時(shí),保證每個(gè)線程拿到一個(gè)進(jìn)而去操作,互不不影響。但是這個(gè)對(duì)象其實(shí)并不是共享的。
4 ThreadLocal會(huì)存在內(nèi)存泄露嗎
ThreadLocal實(shí)例的弱引用,當(dāng)前thread被銷毀時(shí),ThreadLocal也會(huì)隨著銷毀被GC回收。