什么是ThreadLocal變量
ThreadLoal 變量垫卤,線程局部變量社牲,同一個 ThreadLocal 所包含的對象,在不同的 Thread 中有不同的副本纺铭。這里有幾點需要注意:
因為每個 Thread 內(nèi)有自己的實例副本寇钉,且該副本只能由當前 Thread 使用。這是也是 ThreadLocal 命名的由來舶赔。
既然每個 Thread 有自己的實例副本扫倡,且其它 Thread 不可訪問,那就不存在多線程間共享的問題竟纳。
ThreadLocal 提供了線程本地的實例撵溃。它與普通變量的區(qū)別在于疚鲤,每個使用該變量的線程都會初始化一個完全獨立的實例副本。ThreadLocal 變量通常被private static修飾缘挑。當一個線程結(jié)束時集歇,它所使用的所有 ThreadLocal 相對的實例副本都可被回收∮锾裕總的來說诲宇,ThreadLocal 適用于每個線程需要自己獨立的實例且該實例需要在多個方法中被使用,也即變量在線程間隔離而在方法或類間共享的場景惶翻。
ThreadLocal實現(xiàn)原理
首先 ThreadLocal 是一個泛型類姑蓝,保證可以接受任何類型的對象。因為一個線程內(nèi)可以存在多個 ThreadLocal 對象吕粗,所以其實是 ThreadLocal 內(nèi)部維護了一個 Map 它掂,這個 Map 不是直接使用的 HashMap ,而是 ThreadLocal 實現(xiàn)的一個叫做 ThreadLocalMap 的靜態(tài)內(nèi)部類溯泣。而我們使用的 get()、set() 方法其實都是調(diào)用了這個ThreadLocalMap類對應(yīng)的 get()榕茧、set() 方法垃沦。例如下面的 set 方法:
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
get方法:
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
return (T)map.get(this);
// Maps are constructed lazily. if the map for this thread
// doesn't exist, create it, with this ThreadLocal and its
// initial value as its only entry.
T value = initialValue();
createMap(t, value);
return value;
}
createMap方法:
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
ThreadLocalMap是個靜態(tài)的內(nèi)部類:
static class ThreadLocalMap {
........
}
最終的變量是放在了當前線程的 ThreadLocalMap 中,并不是存在 ThreadLocal 上用押,ThreadLocal 可以理解為只是ThreadLocalMap的封裝肢簿,傳遞了變量值。
內(nèi)存泄漏問題
實際上 ThreadLocalMap 中使用的 key 為 ThreadLocal 的弱引用蜻拨,弱引用的特點是池充,如果這個對象只存在弱引用,那么在下一次垃圾回收的時候必然會被清理掉缎讼。所以如果 ThreadLocal 沒有被外部強引用的情況下收夸,在垃圾回收的時候會被清理掉的,這樣一來 ThreadLocalMap中使用這個 ThreadLocal 的 key 也會被清理掉血崭。但是卧惜,value 是強引用,不會被清理夹纫,這樣一來就會出現(xiàn) key 為 null 的 value咽瓷。ThreadLocalMap實現(xiàn)中已經(jīng)考慮了這種情況,在調(diào)用 set()舰讹、get()茅姜、remove() 方法的時候,會清理掉 key 為 null 的記錄月匣。如果說會出現(xiàn)內(nèi)存泄漏钻洒,那只有在出現(xiàn)了 key 為 null 的記錄后奋姿,沒有手動調(diào)用 remove() 方法,并且之后也不再調(diào)用 get()航唆、set()卵迂、remove() 方法的情況下掏熬。
使用場景
如上文所述,ThreadLocal 適用于如下兩種場景
每個線程需要有自己單獨的實例
實例需要在多個方法中共享,但不希望被多線程共享
對于第一點呈驶,每個線程擁有自己實例,實現(xiàn)它的方式很多竖共。例如可以在線程內(nèi)部構(gòu)建一個單獨的實例苗胀。ThreadLoca 可以以非常方便的形式滿足該需求。對于第二點享潜,可以在滿足第一點(每個線程有自己的實例)的條件下困鸥,通過方法間引用傳遞的形式實現(xiàn)。ThreadLocal 使得代碼耦合度更低剑按,且實現(xiàn)更優(yōu)雅疾就。擴展:來探討一下最近面試問的ThreadLocal問題
1)存儲用戶Session
一個簡單的用ThreadLocal來存儲Session的例子:
private static final ThreadLocal threadSession = new ThreadLocal();
public static Session getSession() throws InfrastructureException {
Session s = (Session) threadSession.get();
try {
if (s == null) {
s = getSessionFactory().openSession();
threadSession.set(s);
}
} catch (HibernateException ex) {
throw new InfrastructureException(ex);
}
return s;
}
2)解決線程安全的問題
比如Java7中的SimpleDateFormat不是線程安全的,可以用ThreadLocal來解決這個問題:
public class DateUtil {
private static ThreadLocal<SimpleDateFormat> format1 = new ThreadLocal<SimpleDateFormat>() {
@Override
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
}
};
public static String formatDate(Date date) {
return format1.get().format(date);
}
}
這里的DateUtil.formatDate()就是線程安全的了艺蝴。(Java8里的 java.time.format.DateTimeFormatter是線程安全的猬腰,Joda time里的DateTimeFormat也是線程安全的)。
來探討一下最近面試問的ThreadLocal問題
中高級階段開發(fā)者出去面試猜敢,應(yīng)該躲不開ThreadLocal相關(guān)問題姑荷,本文就常見問題做出一些解答,歡迎留言探討缩擂。
ThreadLocal為java并發(fā)提供了一個新的思路鼠冕, 它用來存儲Thread的局部變量, 從而達到各個Thread之間的隔離運行胯盯。它被廣泛應(yīng)用于框架之間的用戶資源隔離懈费、事務(wù)隔離等。
但是用不好會導(dǎo)致內(nèi)存泄漏博脑, 本文重點用于對它的使用過程的疑難解答楞捂, 相信仔細閱讀完后的朋友可以隨心所欲的安全使用它。
內(nèi)存泄漏原因探索
ThreadLocal操作不當會引發(fā)內(nèi)存泄露趋厉,最主要的原因在于它的內(nèi)部類ThreadLocalMap中的Entry的設(shè)計寨闹。
Entry繼承了WeakReference<ThreadLocal<?>>
,即Entry的key是弱引用君账,所以key'會在垃圾回收的時候被回收掉繁堡, 而key對應(yīng)的value則不會被回收, 這樣會導(dǎo)致一種現(xiàn)象:key為null,value有值椭蹄。
key為空的話value是無效數(shù)據(jù)闻牡,久而久之,value累加就會導(dǎo)致內(nèi)存泄漏绳矩。
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
...
}
怎么解決這個內(nèi)存泄漏問題
每次使用完ThreadLocal都調(diào)用它的remove()方法清除數(shù)據(jù)罩润。因為它的remove方法會主動將當前的key和value(Entry)進行清除。
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(); // 清除key
expungeStaleEntry(i); // 清除value
return;
}
}
}
e.clear()用于清除Entry的key翼馆,它調(diào)用的是WeakReference中的方法:this.referent = null
expungeStaleEntry(i)用于清除Entry對應(yīng)的value割以, 這個后面會詳細講。
JDK開發(fā)者是如何避免內(nèi)存泄漏的
ThreadLocal的設(shè)計者也意識到了這一點(內(nèi)存泄漏)应媚, 他們在一些方法中埋了對key=null的value擦除操作严沥。
這里拿ThreadLocal提供的get()方法舉例,它調(diào)用了ThreadLocalMap#getEntry()方法中姜,對key進行了校驗和對null 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);
}
如果key為null, 則會調(diào)用getEntryAfterMiss()方法丢胚,在這個方法中翩瓜,如果k == null , 則調(diào)用expungeStaleEntry(i);方法携龟。
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;
}
expungeStaleEntry(i)方法完成了對key=null 的key所對應(yīng)的value進行賦空兔跌, 釋放了空間避免內(nèi)存泄漏。
同時它遍歷下一個key為空的entry骨宠, 并將value賦值為null, 等待下次GC釋放掉其空間相满。
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;
// 遍歷下一個key為空的entry层亿, 并將value指向null
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;
}
同理, set()方法最終也是調(diào)用該方法(expungeStaleEntry)立美, 調(diào)用路徑: set(T value)->map.set(this, value)->rehash()->expungeStaleEntries()
remove方法remove()->ThreadLocalMap.remove(this)->expungeStaleEntry(i)
這樣做匿又, 也只能說盡可能避免內(nèi)存泄漏, 但并不會完全解決內(nèi)存泄漏這個問題建蹄。比如極端情況下我們只創(chuàng)建ThreadLocal但不調(diào)用set碌更、get、remove方法等洞慎。所以最能解決問題的辦法就是用完ThreadLocal后手動調(diào)用remove().
手動釋放ThreadLocal遺留存儲?你怎么去設(shè)計/實現(xiàn)痛单?
這里主要是強化一下手動remove的思想和必要性,設(shè)計思想與連接池類似劲腿。
包裝其父類remove方法為靜態(tài)方法旭绒,如果是spring項目, 可以借助于bean的聲明周期, 在攔截器的afterCompletion階段進行調(diào)用挥吵。
弱引用導(dǎo)致內(nèi)存泄漏重父,那為什么key不設(shè)置為強引用
這個問題就比較有深度了,是你談薪的小小資本忽匈。
如果key設(shè)置為強引用房午, 當threadLocal實例釋放后, threadLocal=null丹允, 但是threadLocal會有強引用指向threadLocalMap郭厌,threadLocalMap.Entry又強引用threadLocal, 這樣會導(dǎo)致threadLocal不能正常被GC回收嫌松。
弱引用雖然會引起內(nèi)存泄漏沪曙, 但是也有set、get萎羔、remove方法操作對null key進行擦除的補救措施液走, 方案上略勝一籌。
線程執(zhí)行結(jié)束后會不會自動清空Entry的value
一并考察了你的gc基礎(chǔ)贾陷。
事實上缘眶,當currentThread執(zhí)行結(jié)束后, threadLocalMap變得不可達從而被回收髓废,Entry等也就都被回收了巷懈,但這個環(huán)境就要求不對Thread進行復(fù)用,但是我們項目中經(jīng)常會復(fù)用線程來提高性能慌洪, 所以currentThread一般不會處于終止狀態(tài)顶燕。
Thread和ThreadLocal有什么聯(lián)系呢
ThreadLocal的概念。
Thread和ThreadLocal是綁定的冈爹, ThreadLocal依賴于Thread去執(zhí)行涌攻, Thread將需要隔離的數(shù)據(jù)存放到ThreadLocal(準確的講是ThreadLocalMap)中, 來實現(xiàn)多線程處理。
相關(guān)問題擴展
加分項來了频伤。
spring如何處理bean多線程下的并發(fā)問題
ThreadLocal天生為解決相同變量的訪問沖突問題恳谎, 所以這個對于spring的默認單例bean的多線程訪問是一個完美的解決方案。spring也確實是用了ThreadLocal來處理多線程下相同變量并發(fā)的線程安全問題憋肖。
spring 如何保證數(shù)據(jù)庫事務(wù)在同一個連接下執(zhí)行的
要想實現(xiàn)jdbc事務(wù)因痛, 就必須是在同一個連接對象中操作, 多個連接下事務(wù)就會不可控岸更, 需要借助分布式事務(wù)完成鸵膏。那spring 如何保證數(shù)據(jù)庫事務(wù)在同一個連接下執(zhí)行的呢?
DataSourceTransactionManager 是spring的數(shù)據(jù)源事務(wù)管理器怎炊, 它會在你調(diào)用getConnection()的時候從數(shù)據(jù)庫連接池中獲取一個connection较性, 然后將其與ThreadLocal綁定用僧, 事務(wù)完成后解除綁定。這樣就保證了事務(wù)在同一連接下完成赞咙。
概要源碼:
1.事務(wù)開始階段:org.springframework.jdbc.datasource.DataSourceTransactionManager#doBegin->TransactionSynchronizationManager#bindResource->org.springframework.transaction.support.TransactionSynchronizationManager#bindResource
2.事務(wù)結(jié)束階段:
org.springframework.jdbc.datasource.DataSourceTransactionManager#doCleanupAfterCompletion->TransactionSynchronizationManager#unbindResource->org.springframework.transaction.support.TransactionSynchronizationManager#unbindResource->TransactionSynchronizationManager#doUnbindResource
參考:
https://mp.weixin.qq.com/s/xMWiRDDLfCy4Vsa2jZBSaA
如有侵權(quán)责循,請聯(lián)系我刪除