前言
面試的時候被問到ThreadLocal的相關知識,沒有回答好(奶奶的,現在感覺問啥都能被問倒),所以我決定先解決這幾次面試中都遇到的高頻問題,把這幾個硬骨頭都能理解的透徹的說出來了,感覺最起碼不能總是一輪游。
ThreadLocal介紹
ThreadLocal是JDK1.2開始就提供的一個用來存儲線程本地變量的類。ThreadLocal中的變量是在每個線程中獨立存在的,當多個線程訪問ThreadLocal中的變量的時候,其實都是訪問的自己當前線程的內存中的變量,從而保證的變量的線程安全。
我們一般在使用ThreadLocal的時候都是為了解決線程中存在的變量競爭問題。其實解決這類問題,通常大家也會想到使用synchronized來加鎖解決。
例如在解決SimpleDateFormat的線程安全的時候。SimpleDateFormat是非線程安全的,它里面無論的是format()方法還是parse()方法,都有使用它自己內部的一個Calendar類的對象,format方法是設置時間,parse()方法里面是先調用Calendar的clear()方法,然后又調用了Calendar的set()方法(賦值),如果一個線程剛調用了set()進行賦值,這個時候又來了一個線程直接調用了clear()方法,那么這個parse()方法執行的結果就會有問題的。
解決辦法一
將使用SimpleDateformat的方法加上synchronized,這樣雖然保證了線程安全,但卻降低了效率,同一時間只有一個線程能使用格式化時間的方法。
private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static synchronized String formatDate(Date date){
return simpleDateFormat.format(date);
}
解決辦法二
將SimpleDateFormat的對象,放到ThreadLocal里面,這樣每個線程中都有一個自己的格式對象的副本了。互不干擾,從而保證了線程安全。
private static final ThreadLocal<SimpleDateFormat> simpleDateFormatThreadLocal = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
public static String formatDate(Date date){
return simpleDateFormatThreadLocal.get().format(date);
}
ThreadLocal的原理
我們先看一下ThreadLocal是怎么使用的。
ThreadLocal<Integer> threadLocal99 = new ThreadLocal<Integer>();
threadLocal99.set(3);
int num = threadLocal99.get();
System.out.println("數字:"+num);
threadLocal99.remove();
System.out.println("數字Empty:"+threadLocal99.get());
運行結果:
數字:3
數字Empty:null
使用起來很簡單,主要是將變量放到ThreadLocal里面,在線程執行過程中就可以取到,當執行完成后在remove掉就可以了,只要沒有調用remove()當前線程在執行過程中都是可以拿到變量數據的。
因為是放到了當前執行的線程中,所以ThreadLocal中的變量值只能當前線程來使用,從而保證的了線程安全(當前線程的子線程其實也是可以獲取到的)。
來看一下ThreadLocal的set()方法源碼
public void set(T value) {
// 獲取當前線程
Thread t = Thread.currentThread();
// 獲取ThreadLocalMap
ThreadLocal.ThreadLocalMap map = getMap(t);
// ThreadLocalMap 對象是否為空,不為空則直接將數據放入到ThreadLocalMap中
if (map != null)
map.set(this, value);
else
createMap(t, value); // ThreadLocalMap對象為空,則先創建對象,再賦值。
}
我們看到變量都是存放在了ThreadLocalMap這個變量中的。那么ThreadLocalMap又是怎么來的呢?
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
public class Thread implements Runnable {
... ...
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
... ...
}
通過上面的源碼,我們發現ThreadLocalMap變量是當前執行線程中的一個變量,所以說,ThreadLocal中存放的數據其實都是放到了當前執行線程中的一個變量里面了。也就是存儲在了當前的線程對象里了,別的線程里面是另一個線程對象了,拿不到其他線程對象中的數據,所以數據自然就隔離開了。
那么ThreadLocalMap是怎么存儲數據的呢?
ThreadLocalMap 是ThreadLocal類里的一個內部類,雖然類的名字上帶着Map但卻沒有實現Map接口,只是結構和Map類似而已。
ThreadLocalMap內部其實是一個Entry數組,Entry是ThreadLocalMap中的一個內部類,繼承自WeakReference,並將ThreadLocal類型的對象設置為了Entry的Key,以及對Key設置成弱引用。
ThreadLocalMap的內部數據結構,就大概是這樣的key,value組成的Entry的數組集合。
和真正的Map還是有區別的,沒有鏈表了,這樣在解決key的hash沖突的時候措施肯定就和HashMap不一樣了。
一個線程中是可以創建多個ThreadLocal對象的,多個ThreadLocal對象就會存放多個數據,那么在ThreadLocalMap中就會以數組的形式存放這些數據。
我們來看一下具體的ThreadLocalMap的set()方法的源碼
/**
* 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();
// 如果當前位置不為空,並且當前位置的key和傳過來的key相等,那么就會覆蓋當前位置的數據
if (k == key) {
e.value = value;
return;
}
// 如果當前位置為空,則初始化一個Entry對象,放到當前位置。
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
// 如果當前位置不為空,並且當前位置的key也不等於要賦值的key ,那么將去找下一個空位置,直接將數據放到下一個空位置處。
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
我們從set()方法中可以看到,處理邏輯有四步。
-
第一步先根據Threadlocal對象的hashcode和數組長度做與運算獲取數據應該放在當前數組中的位置。
-
第二步就是判斷當前位置是否為空,為空的話就直接初始化一個Entry對象,放到當前位置。
-
第三步如果當前位置不為空,而當前位置的Entry中的key和傳過來的key一樣,那么直接覆蓋掉當前位置的數據。
-
第四步如果當前位置不為空,並且當前位置的Entry中的key和傳過來的key
也不一樣,那么就會去找下一個空位置,然后將數據存放到空位置(數組超過長度后,會執行擴容的);
在get的時候也是類似的邏輯,先通過傳入的ThreadLocal的hashcode獲取在Entry數組中的位置,然后拿當前位置的Entry的Key和傳入的ThreadLocal對比,相等的話,直接把數據返回,如果不相等就去判斷和數組中的下一個值的key是否相等。。。
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);
}
/**
* 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里面的真正的對象數據,其實是保存在堆里面的,而線程里面只是存儲了對象的引用而已。
並且我們在使用的時候通常需要在上一個線程執行的方法的上下文共享ThreadLocal中的變量。
例如我的主線程是在某個方法中執行代碼呢,但是這個方法中有一段代碼時新創建了一個線程,在這個線程里面還使用了我這個正在執行的方法里面的定義的ThreadLocal里面的變量。這個時候,就是需要從新線程里面調用外面線程的數據,這個就需要線程間共享了。這種子父線程共享數據的情況,ThreadLocal也是支持的。
例如:
ThreadLocal threadLocalMain = new InheritableThreadLocal();
threadLocalMain.set("主線程變量");
Thread t = new Thread() {
@Override
public void run() {
super.run();
System.out.println( "現在獲取的變量是 =" + threadLocalMain.get());
}
};
t.start();
運行結果:
現在獲取的變量是 =主線程變量
上面這樣的代碼就能實現子父線程共享數據的情況,重點是使用InheritableThreadLocal來實現的共享。
那么它是怎么實現數據共享的呢?
在Thread類的init()方法中有這么一段代碼:
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
這段代碼的意思是,在創建線程的時候,如果當前線程的inheritThreadLocals變量和父線程的inheritThreadLocals變量都不為空的時候,會將父線程的inheritThreadLocals變量中的數據,賦給當前線程中的inheritThreadLocals變量。
ThreadLocal的內存泄漏問題
上文我們也提到過,ThreadLocal中的ThreadLocalMap里面的Entry對象是繼承自WeakReference類的,說明Entry的key是一個弱引用。
弱引用是用來描述那些非必須的對象,弱引用的對象,只能生存到下一次垃圾收集發生為止。當垃圾收集器開始工作,無論當前內存是否足夠,都會回收掉只被弱引用關聯的對象。
這個弱引用還是ThreadLocal對象本身,所以一般在線程執行完成后,ThreadLocal對象就會變成null了,而為null的弱引用對象,在下一次GC的時候就會被清除掉,這樣Entry的Key的內存空間就被釋放出來了,但是Entry的value還在占用的內存,如果線程是被復用的(例如線程池中的線程),后面也不使用ThreadLocal存取數據了,那么這里面的value值會一直存在,最終就導致了內存泄漏。
防止內存泄漏的辦法就是在每次使用完ThreadLocal的時候都去執行以下remove()方法,就可以把key和value的空間都釋放了。
那既然容易產生內存泄漏,為什么還要設置成弱引用的呢?
如果正常情況下應該是強引用,但是強引用只要引用關系還在就一直不會被回收,所以如果線程被復用了,那么Entry中的Key和Value都不會被回收,這樣就造成了Key和Value都會發生內存泄漏了;
但是設置成弱引用,當ThreadLocal對象,沒有被強引用后,就會被回收,回收后,Entry中的key就會被設置成null了,如果Thread被重復使用,只要還會用ThreadLocal存儲數據,那么就會調用ThreadLocal的,set、get等方法,在調用set、get、等方法的時候,是會掃描Entry中key為null的數據的。
當發現Entry中,有key為null的數據時,會將value也設置為null,這樣就將value的值也進行了回收,能進一步防止內存泄漏了,並且在進行rehash的時候,也是先清除掉key是null的數據后,如果空間還不夠,才進行擴容的。
但是雖然將key設置了弱引用,但是如果一個線程被重復利用,執行完任務后,再也不使用ThreadLocal了,那么最后value值會一直存在,最終也是會導致內存泄漏的,所以使用ThreadLocal的時候,最后一定要執行remove()方法。