簡述:
在Java并發(fā)中通惫,如果對于某些對象并不需要做共享操作茂翔,而是希望每個線程把對應的對象復制一份到線程內,加上線程天然的隔離性讽膏,這樣可以完美的避免多個線程搶奪操作同一個對象從而報錯檩电。
ThreadLocal就是為了這個場景而產(chǎn)生的。
ThreadLocal VS Synchronized
ThreadLocal和Synchronized都是為了保證多線程場景下的線程安全府树,但是兩者也有著本質的區(qū)別俐末。
ThreadLocal用于處理變量為不共享,其實現(xiàn)原理其實就是將某些對象納入線程中奄侠,這樣對于某個公共的變量卓箫,如果有十個線程需要操作該對象,每個對象都將該變量Copy一份放入線程內垄潮,配合線程天然的隔離性可以避免多個線程搶奪共享變量的問題烹卒。
Synchronized用于處理變量共享導致的線程不安全問題,通過Synchronized鎖可以保證多線程的可見性弯洗、事務一致性旅急、順序性。簡而言之牡整,當你需要安全的處理多線程使用的共享變量時且需要線程之間該變量的互通(而不是簡單的Copy副本各自處理)那么可以使用重量級鎖Synchronized藐吮。
聊一下ThreadLocal實現(xiàn)原理
下圖簡單的反應一下:Thread、ThreadLocalMap、ThredLocal谣辞、Entry
查看Thread類會發(fā)現(xiàn)迫摔,在Thread類中有一個全局變量:
// ThreadLocalMap是ThreadLocal的一個靜態(tài)內部類
ThreadLocal.ThreadLocalMap threadlocals;
那我們就順著這個思路來聊一下原理,先聊一下ThreadLocalMap對象是如何掛載到線程類并且之后線程是如何獲取對應相關聯(lián)的ThreadLocalMap的泥从。之后再去聊一下ThreadLocalMap內部的處理機制句占。
線程如何和ThreadLocalMap關聯(lián)
在Thread類中有如下源碼:
也就是說在線程內部有一個變量threadLocals。每個線程初始化時躯嫉,該變量的默認值都為null纱烘。
那么ThreadLocalMap是何時以及如何會與Thread線程的threadLocals相關聯(lián)呢?
其實這里也使用的是一種懶漢思想祈餐,也就是說凹炸,在Thread被創(chuàng)建之后,代碼并不會自動的創(chuàng)建ThreadLocalMap對象并與Thread關聯(lián)昼弟,而是在使用到線程中的ThreadLocal時才會去關聯(lián)啤它,比如,我們以threadLocal.set操作為例舱痘,投過源碼分析:
@Slf4j
public class ThreadLocalTest {
ThreadLocal<Object> threadLocals = new ThreadLocal<>(); // 往當前線程的ThreadLocal掛載
@Test
public void t() {
threadLocals.set("1");
}
}
在測試類中調用了set方法变骡,斷點跟蹤一下,看看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);
}
對上述set(T value)方法做說明:
- 首先獲取當前線程并賦值給變量t
- 根據(jù)t變量調用本類的getMap方法用來獲取ThreadLocalMap對象芭逝。繼續(xù)瞅一眼getMap干了啥:
/**
* 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;
}
怎么樣塌碌?其實也就是根據(jù)線程t獲取其內部變量threadLocals。
- 緊接著就跟了一個if分支旬盯,分兩種情況台妆,當map == null時候則內部調用createMap方法,如果map不為空胖翰,那么就直接調用ThreadLocalMap.set方法進行賦值操作(由于這個地方主要講的是ThreadLocalMap和Thread的掛載問題接剩,因此map.set(this,value)放到下文詳細描述),主要看一下createMap(t,value)是如何創(chuàng)建ThreadLocalMap對象又如何掛載到Thread上的萨咳,看下面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);
}
發(fā)現(xiàn)懊缺,其內部創(chuàng)建出了ThreadLocalMap對象并將其掛載到Thread類上,參數(shù)為ThreadLocal當前對象this和setValue方法中的Value方法(當然是這樣培他,可以設想在第一次調用ThreadLocal.set方法的時候如果ThreadLocalMap為空則創(chuàng)建鹃两,創(chuàng)建完畢一定是需要緊接著存Value)∫荩看到這里我們知道了ThreadMap和Thread是如何掛載的俊扳。
其實我只是拿ThreadLocal.set操作為例,其實同樣ThreadLocal.get操作也同樣先判斷線程中的ThreadLocalMap是否為空猛遍,若不為空則會調用createMap的方式來進行創(chuàng)建馋记。
ThreadLocalMap類
簡述:在看完了ThreadLocalMap如何與Thread進行掛鉤的碎绎,其實背后原理很簡單,就是一個ThreadLocalMap對象被賦值給了Thread中的threadlocals變量抗果。
所以最核心的代碼其實都在ThreadLocal類和ThreadLocalMap類中(ThreadLocalMap為ThreadLocal的一個靜態(tài)內部類)
我們還是以ThreadLocal.set(Object value)為例,來闡述奸晴,我們想要存入ThreadLocal的值是保存在哪的冤馏。
/**
* 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);
}
這段代碼熟悉吧?這個就是ThreadLocal.set方法寄啼,上文已經(jīng)詳細說明了createMap方法逮光,現(xiàn)在來看一下map.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();
if (k == key) {
e.value = value;
return;
}
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
需要注意的是:ThreadLocalMap他內部的本質其實是一個Entry[]數(shù)組墩划,也就是說涕刚,在ThreadLocalMap中其實并沒有使用ConcurrentHashMap等線程安全的相關數(shù)據(jù)結構,而是通過Entry數(shù)組結合Hash(key)&Entry.length
的方式進行對Entry數(shù)組讀寫乙帮。
這一點在讀源碼的時候需要注意杜漠。
看一下ThreadLocal中對Entry[]的定義:
在回來看set核心代碼,其他他就是先根據(jù)set的第一個參數(shù):key(屬于ThreadLocal)然后和Entry當前的容量做
&
操作察净。然后得到i變量并作為Enry的數(shù)組下標訪問到Entry[i]中的Entry對象驾茴。這個Entry對象就是我們最終需要的,Entry的Key為ThreadLocal對象氢卡,Value為我們保存的Value锈至。拿到這個最終的Entry之后我們就可以做相關的get和set操作了。看了這么多译秦,再去看一下上述文章的createMap方法中new ThreadLocalMap方法峡捡,看看在初始化的時候是如何決定將當前的ThreadLocal放入到Entry[]數(shù)組中的哪個下標的,看源碼:
/**
* Construct a new map initially containing (firstKey, firstValue).
* ThreadLocalMaps are constructed lazily, so we only create
* one when we have at least one entry to put in it.
*/
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
INITIAL_CAPACITY:16
其實這個地方的
threshold:INITIAL_CAPACITY * 2 / 3
INITIAL_CAPACITY是不是很熟悉筑悴,其實和Map數(shù)據(jù)結構中也有類似的字段们拙。
而threshold就類似于Map中的負載因子了。而上述代碼的ThreadLocal.set方法中阁吝,就有在某些場景下調用refresh方法睛竣,因為Entry是以數(shù)組出現(xiàn)的,所以自然而然的想到求摇,這個地方的擴容其實和JDK中的ArrayList動態(tài)擴容是一個樣子了射沟。
面試題
在ThreadLocalMap中內部類Entry為何對ThreadLocal采用弱引用的方式?
答:Entry對于ThreadLocal采用弱引用是為了更好的方便ThreadLocal的GC操作与境;
在如下情況:當不想再去使用ThreadLocal的時候验夯,正常情況下,我們可以將ThreadLocal的外部引用置為null摔刁,這樣可以輔助下次GC的時候回收掉ThreadLocal變量挥转。但是此刻如果對應的Thread一直處于運行狀態(tài),那么ThreadLocal存在于這樣的一條強引用鏈:Thread -> ThreadLocalMap -> Entry -> ThreadLocal。因此對于ThreadLocal的兩條強引用鏈中只要有一方?jīng)]有斷開绑谣,那么GC在多次也無法對ThreadLocal進行回收党窜。在這樣的情況下,在Entry中使用對ThreadLocal的弱引用借宵,只要Java程序中將ThreadLocal引用置為null幌衣,那么該ThreadLocal將不再存在強引用關系,下次GC可以對ThreadLocal對象進行回收壤玫。ThreadLocalMap中的Entry對ThreadLocal采用了弱引用的方式方便GC豁护,那為何還會出現(xiàn)內存泄漏的問題?是什么對象可能發(fā)生泄漏欲间?如何解決的楚里?
答:在思考了第一個問題之后,會發(fā)現(xiàn)猎贴,ThreadLocal確實更加容易回收了班缎,比如只要發(fā)生GC且用戶程序中也沒有對ThreadLocal進行強引用,那么ThreadLocal對象便會被回收她渴。
但問題是:在ThreadLocal被回收之后吝梅,Entry中就會存在這樣的一對數(shù)據(jù)<null,value>,又因為存在如下強引用鏈惹骂,導致GC時Value無法被回收:Thread->ThreadLocalMap->Entry->Value苏携;直到Thread線程終止。
此時在編程上如果不加以特殊處理对粪,那么這樣的value值將永遠無法被回收右冻。ThreadLocal中采用的方法是:在set、get著拭、remove方法中每一次操作都會手動將Entry中key為null的value也置為null纱扭,方便在下一次GC的時候進行回收。
所以在釋放ThreadLocal對象之前儡遮,最好先調用一次remove將value先清空掉乳蛾,否則先釋放了ThreadLocal對象則無法再調用ThreadLocal中的任何方法了。
如下代碼:
try {
// 業(yè)務代碼
} finally {
threadLocal.remove();
threadLocal = null;
}
- 在使用ThreadLocal過程中鄙币,在當前線程下創(chuàng)建子線程肃叶,子線程無法獲取父線程的數(shù)據(jù),如何解決十嘿?
答:因為子線程對象和父線程對象肯定不是同一個因惭,在ThreadLocal中根據(jù)Thread對象獲取到的Entry對象自然也就不同。
可以使用ThreadLocal的子類:InheritableThreadLocal绩衷;當一個線程進行創(chuàng)建子線程的過程中蹦魔,父線程會將自身的InheritableThreadLocal變量中的數(shù)據(jù)全部傳遞給子線程的InheritableThreadLocal激率。因此子線程也可以使用父線程ThreadLocal中的數(shù)據(jù)了。見如下代碼:
private static ThreadLocal local = new ThreadLocal();
private static InheritableThreadLocal inheritableThreadLocal = new InheritableThreadLocal();
public static void main(String[] args) throws InterruptedException {
local.set("ThreadLocal");
inheritableThreadLocal.set("InheritableThreadLocal");
new Thread(() -> {
System.out.println(local.get());
System.out.println(inheritableThreadLocal.get());
}).start();
Thread.sleep(20000);
}
// 結果:
null
InheritableThreadLocal