1. 什麼是ThreadLocal
ThreadLocal提供了線程的局部變量,每個線程都可以通過set()和get()來對這個局部變量進行操作,但不會和其他線程的局部變量進行衝突,實現了線程的數據隔離~。
簡要言之:往ThreadLocal中填充的變量屬於當前線程,該變量對其他線程而言是隔離的。
ThreadLocal用在什麼地方?
討論ThreadLocal用在什麼地方前,我們先明確下,如果僅僅就一個線程,那麼都不用談ThreadLocal的,ThreadLocal是用在多線程的場景的!!!
ThreadLocal歸納下來就2類用途:
-
保存線程上下文信息,在任意需要的地方可以獲取!!!
-
線程安全的,避免某些情況需要考慮線程安全必須同步帶來的性能損失!!!
保存線程上下文信息,在任意需要的地方可以獲取!!!
由於ThreadLocal的特性,同一線程在某地方進行設置,在隨後的任意地方都可以獲取到。從而可以用來保存線程上下文信息。
常用的比如每個請求怎麼把一串後續關聯起來,就可以用ThreadLocal進行set,在後續的任意需要記錄日誌的方法裏面進行get獲取到請求id,從而把整個請求串起來。
還有比如Spring的事務管理,用ThreadLocal存儲Connection,ThreadLocal能夠實現當前線程的操作都是用同一個Connection,保證了事務!從而各個DAO可以獲取同一Connection,可以進行事務回滾,提交等操作。
同樣的,Hibernate對Connection的管理也是採用了相同的手法(使用ThreadLocal,當然了Hibernate的實現是更強大的)~
線程安全的,避免某些情況需要考慮線程安全必須同步帶來的性能損失!!!
每個線程往ThreadLocal中讀寫數據是線程隔離,互相之間不會影響的,所以ThreadLocal無法解決共享對象的更新問題!
由於不需要共享信息,自然就不存在競爭問題了,從而保證了某些情況下線程的安全,以及避免了某些情況需要考慮線程安全必須同步帶來的性能損失!!!
這類場景阿里規範裏面也提到了:
ThreadLocal一些細節!
ThreaLocal使用示例代碼:
public class ThreadLocalTest {
private static ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
public static void main(String[] args) {
new Thread(() -> {
try {
for (int i = 0; i < 100; i++) {
threadLocal.set(i);
System.out.println(Thread.currentThread().getName() + "====" + threadLocal.get());
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} finally {
threadLocal.remove();
}
}, "threadLocal1").start();
new Thread(() -> {
try {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + "====" + threadLocal.get());
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} finally {
threadLocal.remove();
}
}, "threadLocal2").start();
}
}
代碼運行結果:
從運行的結果我們可以看到threadLocal1進行set值對threadLocal2並沒有任何影響!
Thread、ThreadLocalMap、ThreadLocal總覽圖:
Thread類有屬性變量threadLocals (類型是ThreadLocal.ThreadLocalMap),也就是說每個線程有一個自己的ThreadLocalMap ,所以每個線程往這個ThreadLocal中讀寫隔離的,並且是互相不會影響的。
一個ThreadLocal只能存儲一個Object對象,如果需要存儲多個Object對象那麼就需要多個ThreadLocal!!!
如圖:
看到上面的幾個圖,大概思路應該都清晰了,我們Entry的key指向ThreadLocal用虛線表示弱引用 。
2. ThreadLocal實現的原理
首先,我們來看一下ThreadLocal的set()方法,因爲我們一般使用都是new完對象,就往裏邊set對象了
public void set(T value)
{
// 得到當前線程對象
Thread t = Thread.currentThread();
// 這裏獲取ThreadLocalMap
ThreadLocalMap map = getMap(t);
// 如果map存在,則將當前線程對象t作爲key,要存儲的對象作爲value存到map裏面去
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
上面有個ThreadLocalMap,我們去看看這是什麼?
static class ThreadLocalMap
{
/**
* The entries in this hash map extend WeakReference, using
* its main ref field as the key (which is always a
* ThreadLocal object). Note that null keys (i.e. entry.get()
* == null) mean that the key is no longer referenced, so the
* entry can be expunged from table. Such entries are referred to
* as "stale entries" in the code that follows.
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
//....很長
}
通過上面我們可以發現的是ThreadLocalMap是ThreadLocal的一個內部類。用Entry類來進行存儲
我們的值都是存儲到這個Map上的,key是當前ThreadLocal對象!
如果該Map不存在,則初始化一個。如果該Map存在,則從Thread中獲取!
Thread維護了ThreadLocalMap變量 。
可以看出,ThreadLocalMap是在ThreadLocal中使用內部類來編寫的,但對象的引用是在Thread中!
於是我們可以總結出:Thread爲每個線程維護了ThreadLocalMap這麼一個Map,而ThreadLocalMap的key是LocalThread對象本身,value則是要存儲的對象。
有了上面的基礎,我們看get()方法就一點都不難理解了:
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();
}
ThreadLocal原理總結
(1)每個Thread維護着一個ThreadLocalMap的引用
(2)ThreadLocalMap是ThreadLocal的內部類,用Entry來進行存儲
(3)調用ThreadLocal的set()方法時,實際上就是往ThreadLocalMap設置值,key是ThreadLocal對象,值是傳遞進來的對象
(4)調用ThreadLocal的get()方法時,實際上就是往ThreadLocalMap獲取值,key是ThreadLocal對象
(5)ThreadLocal本身並不存儲值,它只是作爲一個key來讓線程從ThreadLocalMap獲取value。
正因爲這個原理,所以ThreadLocal能夠實現“數據隔離”,獲取當前線程的局部變量值,不受其他線程影響~
3. 避免內存泄露
我們來看一下ThreadLocal的對象關係引用圖:
我們Entry的key指向ThreadLocal用虛線表示弱引用。
java對象的引用包括 :強引用,軟引用,弱引用,虛引用 。
因爲這裏涉及到弱引用,簡單說明下:
弱引用也是用來描述非必需對象的,當JVM進行垃圾回收時,無論內存是否充足,該對象僅僅被弱引用關聯,那麼就會被回收。
當僅僅只有ThreadLocalMap中的Entry的key指向ThreadLocal的時候,ThreadLocal會進行回收的!!!
ThreadLocal被垃圾回收後,在ThreadLocalMap裏對應的Entry的鍵值會變成null,但是Entry是強引用,那麼Entry裏面存儲的Object,並沒有辦法進行回收,所以ThreadLocalMap 做了一些額外的回收工作。
ThreadLocal內存泄漏的根源是:由於ThreadLocalMap的生命週期跟Thread一樣長,如果沒有手動刪除對應key就會導致內存泄漏,而不是因爲弱引用。
想要避免內存泄露就要手動remove()掉。
ThreadLocal的最佳實踐!
ThreadLocal被垃圾回收後,在ThreadLocalMap裏對應的Entry的鍵值會變成null,但是Entry是強引用,那麼Entry裏面存儲的Object,並沒有辦法進行回收,所以ThreadLocalMap 做了一些額外的回收工作。
備註:很多時候,我們都是用在線程池的場景,程序不停止,線程基本不會銷燬!!!
由於線程的生命週期很長,如果我們往ThreadLocal裏面set了很大很大的Object對象,雖然set、get等等方法在特定的條件會調用進行額外的清理,但是ThreadLocal被垃圾回收後,在ThreadLocalMap裏對應的Entry的鍵值會變成null,但是後續在也沒有操作set、get等方法了。
所以最佳實踐,應該在我們不使用的時候,主動調用remove方法進行清理。
這裏把ThreadLocal定義爲static還有一個好處就是,由於ThreadLocal有強引用在,那麼在ThreadLocalMap裏對應的Entry的鍵會永遠存在,那麼執行remove的時候就可以正確進行定位到並且刪除!!!
最佳實踐做法應該爲:
抽象爲:
try {
// 其它業務邏輯
} finally {
threadLocal對象.remove();
}