ThreadLocal和Synchonized都用語解決多線程并發(fā)訪問的,可以ThreadLocal與Synchonzied有本質(zhì)的差別,synchoronized是利用鎖的機(jī)制,使變量或代碼塊僅僅能被一個(gè)線程訪問。而ThreadLoacal為每個(gè)線程都提供了變量的副本,使得每個(gè)線程在某個(gè)時(shí)間訪問到,這樣對線程間的數(shù)據(jù)進(jìn)行了隔離。

在Android開發(fā)中Looper是通過ThreadLocal對looper和不同線程進(jìn)行隔離,使每個(gè)線程中都存在一個(gè)Looper。
static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
private static void prepare(boolean quitAllowed) {
if (sThreadLocal.get() != null) {
throw new RuntimeException("Only one Looper may be created per thread");
}
sThreadLocal.set(new Looper(quitAllowed));
}
ThreadLocal的使用
ThreadLocal類接口很簡單,只有4個(gè)方法。
void set(Object value)
設(shè)置當(dāng)前線程的線程局部變量的值。
public Object get()
該方法返回當(dāng)前線程所對應(yīng)的線程局部變量。
public void remove()
將當(dāng)前線程局部變量的值刪除,目的是為了減少內(nèi)存的占用,該方法是JDK 5.0新增的方法。需要指出的是,當(dāng)線程結(jié)束后,對應(yīng)該線程的局部變量將自動被垃圾回收,所以顯式調(diào)用該方法清除線程的局部變量并不是必須的操作,但它可以加快內(nèi)存回收的速度。
protected Object initialValue()
返回該線程局部變量的初始值,該方法是一個(gè)protected的方法,顯然是為了讓子類覆蓋而設(shè)計(jì)的。這個(gè)方法是一個(gè)延遲調(diào)用方法,在線程第1次調(diào)用get()或set(Object)時(shí)才執(zhí)行,并且僅執(zhí)行1次。ThreadLocal中的缺省實(shí)現(xiàn)直接返回一個(gè)null。
ThreadLocal解析
/**
* 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();
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
/**
* 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);
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
ThreadLocal.ThreadLocalMap threadLocals = null;
上面先取到當(dāng)前線程,然后調(diào)用getMap方法獲取對應(yīng)的ThreadLocalMap,ThreadLocalMap是ThreadLocal的靜態(tài)內(nèi)部類,然后Thread類中有一個(gè)這樣類型成員,所以getMap是直接返回Thread的成員。
/**
* 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 {
static class Entry extends WeakReference<ThreadLocal> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal k, Object v) {
super(k);
value = v;
}
}
private static final int INITIAL_CAPACITY = 16;
private Entry[] table;
}
看下ThreadLocal的內(nèi)部類ThreadLocalMap源碼:
可以看到有個(gè)Entry內(nèi)部靜態(tài)類,它繼承了WeakReference,總之它記錄了兩個(gè)信息,一個(gè)是ThreadLocal<?>類型,一個(gè)是Object類型的值。getEntry方法則是獲取某個(gè)ThreadLocal對應(yīng)的值,set方法就是更新或賦值相應(yīng)的ThreadLocal對應(yīng)的值。
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);
}
ThreadLocal中的get方法,其實(shí)就是拿到每個(gè)線程獨(dú)有的ThreadLocalMap
然后再用ThreadLocal的當(dāng)前實(shí)例,拿到Map中的相應(yīng)的Entry,然后就可以拿到相應(yīng)的值返回出去。當(dāng)然,如果Map為空,還會先進(jìn)行map的創(chuàng)建,初始化等工作。
/**
* 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;
}
利用getEntryAfterMiss方法找到Entry或者調(diào)用expungeStaleEntry方法處理key為null。這里nextIndex去處理當(dāng)前hash沖突。
Hash沖突怎么解決
與Hashmap不同,ThreadLocal用的是一種線性探測的方式處理,根據(jù)hashcode值確定元素在table數(shù)組中的位置,如果發(fā)現(xiàn)這個(gè)位置已經(jīng)有其他元素和位置沖突就存放下一個(gè)為空的位置。
private void set(ThreadLocal<?> key, Object value) {
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)]) {
...
}...
/**
* Increment i modulo len.
*/
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
/**
* Decrement i modulo len.
*/
private static int prevIndex(int i, int len) {
return ((i - 1 >= 0) ? i - 1 : len - 1);
}
擴(kuò)容操作
private void rehash() {
expungeStaleEntries();
// Use lower threshold for doubling to avoid hysteresis
if (size >= threshold - threshold / 4)
resize();
}
private void resize() {
Entry[] oldTab = table;
int oldLen = oldTab.length;
int newLen = oldLen * 2;
Entry[] newTab = new Entry[newLen];
int count = 0;
for (int j = 0; j < oldLen; ++j) {
Entry e = oldTab[j];
if (e != null) {
ThreadLocal<?> k = e.get();
if (k == null) {
e.value = null; // Help the GC
} else {
int h = k.threadLocalHashCode & (newLen - 1);
while (newTab[h] != null)
h = nextIndex(h, newLen);
newTab[h] = e;
count++;
}
}
}
setThreshold(newLen);
size = count;
table = newTab;
}
/**
* Set the resize threshold to maintain at worst a 2/3 load factor.
*/
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
當(dāng)數(shù)據(jù)超過閾值的四分之三時(shí),擴(kuò)容2倍,閾值設(shè)置為長度的三分之二。
引發(fā)的內(nèi)存泄漏分析
我們可以知道每個(gè)Thread 維護(hù)一個(gè) ThreadLocalMap,這個(gè)映射表的 key是ThreadLocal實(shí)例本身,value 是真正需要存儲的 Object,也就是說 ThreadLocal 本身并不存儲值,它只是作為一個(gè) key 來讓線程從ThreadLocalMap獲取value。

圖中的虛線表示弱引用。
這樣,當(dāng)把threadlocal變量置為null以后,沒有任何強(qiáng)引用指向threadlocal實(shí)例,所以threadlocal將會被gc回收。這樣一來,ThreadLocalMap中就會出現(xiàn)key為null的Entry,就沒有辦法訪問這些key為null的Entry的value,如果當(dāng)前線程再遲遲不結(jié)束的話,這些key為null的Entry的value就會一直存在一條強(qiáng)引用鏈:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value,而這塊value永遠(yuǎn)不會被訪問到了,所以存在著內(nèi)存泄露。
只有當(dāng)前thread結(jié)束以后,current thread就不會存在棧中,強(qiáng)引用斷開,Current Thread、Map value將全部被GC回收。最好的做法是不在需要使用ThreadLocal變量后,都調(diào)用它的remove()方法,清除數(shù)據(jù)。 調(diào)用remove()方法最佳時(shí)機(jī)是線程運(yùn)行結(jié)束之前的finally代碼塊中調(diào)用,這樣能完全避免操作不當(dāng)導(dǎo)致的內(nèi)存泄漏,這種主動清理的方式比惰性刪除有效。
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;
}
這個(gè)方法在ThreadLocal的set、get、remove時(shí)都會被調(diào)用,從上面代碼中,可以看出先清理指定的Entry,再遍歷,如果發(fā)現(xiàn)有Entry的key為null,就清理。Key==null,也就是ThreadLocal對象是null。所以當(dāng)程序中,將ThreadLocal對象設(shè)置為null,在該線程繼續(xù)執(zhí)行時(shí),如果執(zhí)行另一個(gè)ThreadLocal時(shí),就會觸發(fā)該方法。就有可能清理掉Key是null的那個(gè)ThreadLocal對應(yīng)的值。所以說expungStaleEntry()方法清除線程ThreadLocalMap里面所有key為null的value。
/**
* Remove the entry for key.
*/
private void remove(ThreadLocal<?> key) {
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)]) {
if (e.get() == key) {
e.clear();
expungeStaleEntry(i);
return;
}
}
}
在ThreadLocal的實(shí)現(xiàn),我們可以看見,無論是get()、set()在某些時(shí)候,調(diào)用了expungeStaleEntry方法用來清除Entry中Key為null的Value,但是這是不及時(shí)的,也不是每次都會執(zhí)行的,所以一些情況下還是會發(fā)生內(nèi)存泄露。只有remove()方法中顯式調(diào)用了expungeStaleEntry方法。
static class Entry extends WeakReference<ThreadLocal>
由于ThreadLocalMap持有ThreadLocal的弱引用,即使沒有手動刪除,ThreadLocal的對象實(shí)例也會被回收。value在下一次ThreadLocalMap調(diào)用set,get,remove都有機(jī)會被回收。所以說jvm利用弱引用來避免內(nèi)存泄露,通過remove方法回收弱引用。
小結(jié)
key 使用強(qiáng)引用:引用ThreadLocal的對象被回收了,但是ThreadLocalMap還持有ThreadLocal的強(qiáng)引用,如果沒有手動刪除,ThreadLocal的對象實(shí)例不會被回收,導(dǎo)致Entry內(nèi)存泄漏。
key 使用弱引用:引用的ThreadLocal的對象被回收了,由于ThreadLocalMap持有ThreadLocal的弱引用,即使沒有手動刪除,ThreadLocal的對象實(shí)例也會被回收。value在下一次ThreadLocalMap調(diào)用set,get,remove都有機(jī)會被回收
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
使用線程池+ ThreadLocal時(shí)要小心,因?yàn)檫@種情況下,線程是一直在不斷的重復(fù)運(yùn)行的,如果沒有及時(shí)的清理,那么之前對該線程的使用,就會影響到后面的線程了,從而也就造成了value可能造成累積的情況,所以要調(diào)用remove()方法及時(shí)清除來解決。
錯(cuò)誤使用ThreadLocal導(dǎo)致線程不安全
public class ThreadLocalUnsafe implements Runnable {
public static Number number = new Number(0);
public void run() {
//每個(gè)線程計(jì)數(shù)加一
number.setNum(number.getNum() + 1);
//將其存儲到ThreadLocal中
value.set(number);
//輸出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();
}
}
}
public class Number {
private int num;
public int getNum() {
return num;
}
public void setNum(int num) {
this.num = num;
}
@Override
public String toString() {
return "Number [num=" + num + "]";
}
}
代碼中的數(shù)據(jù)會發(fā)生線程不安全的現(xiàn)象(輸出的全是5),static修飾的類在JVM中只保存一個(gè)實(shí)例對象,ThreadLocalMap中保存的其實(shí)是對象的一個(gè)引用,這樣的話,當(dāng)有其他線程對這個(gè)引用指向的對象實(shí)例做修改時(shí),其實(shí)也同時(shí)影響了所有的線程持有的對象引用所指向的同一個(gè)對象實(shí)例,這樣的話會導(dǎo)致每個(gè)線程輸出的內(nèi)容一致,而上面的程序要正常的工作,應(yīng)該的用法是讓每個(gè)線程中的ThreadLocal都應(yīng)該持有一個(gè)新的Number對象。
總結(jié)
ThreadLocal是解決線程安全的一個(gè)很好的思路,它通過為每個(gè)線程提供了一個(gè)獨(dú)立的變量副本解決了額變量并發(fā)訪問的沖突問題。在很多情況下,ThreadLocal比直接使用synchronized同步機(jī)制解決線程安全問題更簡單,更方便,且結(jié)果程序擁有更高的并發(fā)性。ThreadLocal和synchronize用一句話總結(jié)就是一個(gè)用存儲拷貝進(jìn)行空間換時(shí)間,一個(gè)是用鎖機(jī)制進(jìn)行時(shí)間換空間。
本文由本人編寫總結(jié),版權(quán)由享學(xué)課堂所有