目錄:
- 什麼是ThreadLocal
- ThreadLocal使用示例
- 真實案例剖析
- 深入ThreadLocal源碼
- ThreadLocal爲什麼會內存泄漏
- 總結
什麼是ThreadLocal?
ThreadLocal是Java裏一種特殊的變量。有些夥伴喜歡把它和線程同步機制混爲一談,事實上ThreadLocal與線程同步無關。ThreadLocal雖然提供了一種解決多線程環境下成員變量的問題,但是它並不是解決多線程共享變量的問題。
ThreadLocal與線程同步機制不同,線程同步機制是多個線程共享同一個變量,而ThreadLocal是爲每一個線程創建一個單獨的變量副本,故而每個線程都可以獨立地改變自己所擁有的變量副本,而不會影響其他線程所對應的副本。可以說ThreadLocal爲多線程環境下變量問題提供了另外一種解決思路。
注意: ThreadLocal實例本身是不存儲值,它只是提供了一個在當前線程中找到副本值得key
ThreadLocal源碼中有哪些方法?
在這些方法中其實我們主要關注這四個方法:
- get():返回此線程局部變量的當前線程副本中的值。
- initialValue():返回此線程局部變量的當前線程的“初始值”。
- remove():移除此線程局部變量當前線程的值。
- set(T value):將此線程局變量的當前線程副本中的值設置爲指定值。
除了這四個方法,ThreadLocal內部還有一個靜態內部類
- ThreadLocalMap:該內部類纔是實現線程隔離機制的關鍵,get()、set()、remove()都是基於該內部類操作。
ThreadLocalMap提供了一種用鍵值對方式存儲每一個線程的變量副本的方法,key爲當前ThreadLocal對象,value則是對應線程的變量副本。
Thread、ThreadLocal、ThreadLocalMap的關係如下;
Thread
--ThreadLocal(屬於Thread的成員變量,而不是Thread包含在ThreadLocal中,不要搞錯了哦)
--ThreadLocalMap(屬於ThreadLocal的靜態內部類)
ThreadLocal使用示例
public class ThreadLocalTest {
private static ThreadLocal<Integer> total = new ThreadLocal<Integer>(){
// 實現initialValue()
@Override
public Integer initialValue() {
return 0;
}
};
public static int addOne(){
total.set(total.get()+1);
return total.get();
}
public static void main(String[] args) {
Runnable task = () -> {
for(int i=0;i<5;i++){
System.out.println(Thread.currentThread().getName()+" :ThreadLocalTest"+ThreadLocalTest.addOne());
}
};
Thread thread1 = new Thread(task);
Thread thread2 = new Thread(task);
Thread thread3 = new Thread(task);
thread1.start();
thread2.start();
thread3.start();
}
}
運行結果: 從運行結果可以看出,ThreadLocal確實是可以達到線程隔離機制,確保變量的安全性。
看了這個例子可能很多小夥伴還不知道在實際場景是怎麼用的,對吧?那我這裏再講一個實際工作開發中的遇到的一個真實案例;
真實案例剖析:
一天,張大胖上午遇到了一個棘手的問題,他在一個AccountService中寫了一段類似這樣的代碼:
Context ctx = new Context();
ctx.setTrackerID(.....)
然後這個AccountService 調用了其他Java類,不知道經過了多少層調用以後,最終來到了一個叫做AccountUtil的地方,在這個類中需要使用Context中的trackerID來做點兒事情:
很明顯,這個AccountUtil沒有辦法拿到Context對象, 怎麼辦?
張大胖想到,要不把Context對象一層層地傳遞下去,這樣AccountUtil不就可以得到了嗎? 可是這麼做改動量太大!涉及到的每一層函數調用都得改動,有很多類都不屬於自己的小組管理,還得和別人協調。
更要命的是有些類根本就沒有源碼,想改都改不了。
這也難不住我,張大胖想:可以把那個set/get TrackerID的方法改成靜態(static)的,這樣不管跨多少層調用都沒有問題!
public class Context{
public static String getTrackerID(){
......
}
public static void setTrackerID(String id){
......
}
}
這樣就不用一層層地傳遞了,Perfect!
張大胖得意洋洋地把代碼提交給Bill做Review。
Bill看了一眼就指出了致命的問題: 多線程併發的時候出錯!
張大胖恨不得找個地縫鑽進去:又栽在多線程上面了,這次犯的還是低級錯誤!
線程1調用了Context.setTrackerID(), 線程2 也調用了Context.setTrackerID(),數據互相覆蓋,不出亂子纔怪。
張大胖感慨地說:“像我這樣中情況,需要在某處設置一個值,然後經過重重方法調用,到了另外一處把這個值取出來,又要線程安全,實在是不好辦啊, 對了,我能不能把這個值就放到線程中? 讓線程攜帶着這個值到處跑,這樣我無論在任何地方都可以輕鬆獲得了!”
Bill說:“有啊,每個線程都有一個私家領地! 在Thread這個類中有個專門的數據結構,你可以放入你的TrackerID,然後到任何地方都可以把這個TrackerID給取出來。”
“這麼好? ”
張大胖打開JDK中的Thread類,仔細查看,果然在其中有個叫做threadLocals的變量,還是個Map類型 , 但是在Thread類中卻沒有對這個變量操作的方法。
看到張大胖的疑惑,Bill說:“也許你注意到了,這個變量不是通過Thread的訪問的,對他的訪問委託給了ThreadLocal這個類。”
“那我怎麼使用它?”
“非常簡單, 你可以輕鬆創建一個ThreadLocal類的實例:
ThreadLocal<String> threadLocalA= new ThreadLocal<String>();
線程1: threadLocalA.set("1234");
線程2: threadLocalA.set("5678");
像‘1234’, ‘5678’這些值都會放到自己所屬的線程對象中。”
“等你使用的時候,可以這麼辦:” 線程1: threadLocalA.get() --> "1234" 線程2: threadLocalA.get() --> "5678"
“明白了,相當於把各自的數據放入到了各自Thread這個對象中去了,每個線程的值自然就區分開了。 可是我不明白的是爲什麼那個數據結構是個map 呢?”
“你想想,假設你創建了另外一個threadLocalB:”
ThreadLocal<Integer> threadLocalB = new ThreadLocal<Integer>();
線程1: threadLocalB.set(30);
線程2: threadLocalB.set(40);
那線程對象的Map就起到作用了:
“明白了,這個私家領地還真是好用,我現在就把我那個Context給改了,讓它使用ThreadLocal:”
public class Context {
private static final ThreadLocal<String> mThreadLocal
= new ThreadLocal<String>();
public static void setTrackerID(String id) {
mThreadLocal.set(id);
}
public static String getTrackerID() {
return mThreadLocal.get();
}
}
深入ThreadLocal源碼:
ThreadLocalMap是ThreadLocal的匿名內部類,是實現ThreadLocal的關鍵;
ThreadLocalMap其內部利用Entry來實現key-value的存儲,如下: 從上面可以看出:
- ThreadLocalMap是屬於ThreadLocald匿名內部類
- Entry是ThreadLocalMap的匿名內部類
- Entry的key就是ThreadLocal(同時這裏說明了ThreadLocal實例本身是不存儲值,它只是提供了一個在當前線程中找到副本值得key),而value就是具體的值
- Entry也繼承WeakReference,所以說Entry所對應key(ThreadLocal實例)的引用爲一個弱引用(關於弱引用後續會出一篇文章講解一下,請持續關注)
ThreadLocalMap的源碼比較多,我們就看兩個最核心的方法吧!
- set(ThreadLocal key, Object value)
- getEntry()
set(ThreadLocal key, Object value)
/**
* 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;
// 根據 ThreadLocal 的散列值,查找對應元素在數組中的位置
int i = key.threadLocalHashCode & (len-1);
// 採用“線性探測法”,尋找合適位置(解決hash衝突的一種方法)
for (Entry e = tab[i];
e != null;
//查找相鄰的曹
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
//判斷: key 存在,直接覆蓋
if (k == key) {
e.value = value;
return;
}
// key == null,但是存在值(因爲此處的e != null)
//說明之前的ThreadLocal對象已經被回收了
if (k == null) {
// 用新元素替換陳舊的元素
replaceStaleEntry(key, value, i);
return;
}
}
// ThreadLocal對應的key實例不存在也沒有陳舊元素,new 一個
tab[i] = new Entry(key, value);
int sz = ++size;
// cleanSomeSlots 清除老舊的Entry(key == null)
// 如果沒有清理陳舊的 Entry 並且數組中的元素大於了閾值,則進行 rehash
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
這裏的set()操作和我們在集合類的put()方式不一樣的,雖然他們都是key-value結構,不同在於他們解決散列衝突的方式不同。
- 在集合類中Map的put()採用的是拉鍊法
- ThreadLocalMap的set()則是採用開放定址法
set()操作除了存儲元素外,還有一個很重要的作用,就是replaceStaleEntry()和cleanSomeSlots(),這兩個方法可以清除掉key == null 的實例,防止內存泄漏。
set()方法中還有一個很重要變量:threadLocalHashCode,定義如下:
private final int threadLocalHashCode = nextHashCode();
從名字上面我們可以看出threadLocalHashCode應該是ThreadLocal的散列值,定義爲final,表示ThreadLocal一旦創建其散列值就已經確定了,生成過程則是調用nextHashCode():
private static AtomicInteger nextHashCode =
new AtomicInteger();
/**
* The difference between successively generated hash codes - turns
* implicit sequential thread-local IDs into near-optimally spread
* multiplicative hash values for power-of-two-sized tables.
*/
private static final int HASH_INCREMENT = 0x61c88647;
/**
* Returns the next hash code.
*/
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
nextHashCode表示分配下一個ThreadLocal實例的threadLocalHashCode的值,HASH_INCREMENT則表示分配兩個ThradLocal實例的threadLocalHashCode的增量,從nextHashCode就可以看出他們的定義。
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);
}
由於採用了開放定址法,所以當前key的散列值和元素在數組的索引並不是完全對應的,首先取一個探測數(key的散列值),如果所對應的key就是我們所要找的元素,則返回,否則調用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;
}
這裏有一個重要的地方,當key == null時,調用了expungeStaleEntry()方法,該方法用於處理key == null,有利於GC回收,能夠有效地避免內存泄漏。
看完了ThreadLocal的ThreadLocalMap,我們再來看看ThreadLocal的set()和get(), initialValue()方法;
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();
}
首先通過當前線程獲取所對應的成員變量ThreadLocalMap,然後通過ThreadLocalMap獲取當前ThreadLocal的Entry,最後通過所獲取的Entry獲取目標值result。
getMap()方法可以獲取當前線程所對應的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;
}
set(T value)
設置當前線程的線程局部變量的值。
/**
* 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,如果不爲空,則調用ThreadLocalMap的set()方法,key就是當前ThreadLocal,如果不存在,則調用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
*/
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
initialValue()
返回該線程局部變量的初始值。
protected T initialValue() {
return null;
}
- 該方法定義爲protected級別且返回爲null,很明顯是要子類實現它的(子類的修飾範圍要大於父類的修飾符),所以我們在使用ThreadLocal的時候一般都應該覆蓋該方法。
- 該方法不能顯示調用,只有在第一次調用get()或者set()方法時纔會被執行,並且僅執行1次。
remove()
將當前線程局部變量的值刪除。
//該方法的目的是減少內存的佔用。當然,我們不需要顯示調用該方法
//因爲一個線程結束後,它所對應的局部變量就會被垃圾回收。
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
ThreadLocal爲什麼會內存泄漏
前面提到每個Thread都有一個ThreadLocal.ThreadLocalMap的map,該map的key爲ThreadLocal實例,它爲一個弱引用,我們知道弱引用有利於GC回收。當ThreadLocal的key == null時,GC就會回收這部分空間,但是value卻不一定能夠被回收,因爲他還與Current Thread存在一個強引用關係,如下(圖片來自http://www.jianshu.com/p/ee8c9dccc953): 因爲map中存在這個強引用關係,這就會會導致這個value無法被回收。如果這個線程對象不被銷燬,那麼這個強引用關係會一直存在,然後就會出現內存泄漏情況了。所以說只要這個線程對象能夠及時被GC回收,就不會出現內存泄漏。如果碰到線程池,那問題就更大了。
那這樣要怎麼避免這個問題呢?
在前面提過,在ThreadLocalMap中的setEntry()、getEntry(),如果遇到key == null的情況,會對value設置爲null。當然我們也可以顯示調用ThreadLocal的remove()方法進行處理。
小結
下面再對ThreadLocal進行簡單的總結:
ThreadLocal 不是用於解決共享變量的問題的,也不是爲了協調線程同步而存在,而是爲了方便每個線程處理自己的狀態而引入的一個機制。這點至關重要。
每個Thread內部都有一個ThreadLocal.ThreadLocalMap類型的成員變量,該成員變量用來存儲實際的ThreadLocal變量副本。
ThreadLocal並不是爲線程保存對象的副本,它僅僅只起到一個索引的作用。它的主要木得視爲每一個線程隔離一個類的實例,這個實例的作用範圍僅限於線程內部。
參考來源:https://mp.weixin.qq.com/s/L_5WPcX5pzM8GmTpQbytzw
https://mp.weixin.qq.com/s/k4cMqePHagb15-jlYh4PkA
關注微信公衆號【悟能之能】瞭解更多編程技巧。