ThreadLocal是什麼?
ThreadLocal是線程本地變量,可以爲多線程的併發問題提供一種解決方式,當使用ThreadLocal維護變量時,ThreadLocal爲每個使用該變量的線程提供獨立的變量副本,所以每一個線程都可以獨立地改變自己的副本,而不會影響其它線程所對應的副本。
ThreadLocal使用場景
①多個線程去獲取一個共享變量時,要求獲取的是這個變量的初始值的副本。②每個線程存儲這個變量的副本,對這個變量副本的改變不去影響變量本身。③適用於多個線程依賴不同變量值完成操作的場景。
ThreadLocal類常用接口
void set(T value):設置當前線程的線程局部變量的值
T get():獲取當前線程所對應的線程局部變量
void remove():刪除當前線程局部變量的值,目的是爲了減少內存的佔用
T initialValue():該線程局部變量的初始值(默認值爲null),該方法是一個protected的懶加載方法,線程第1次調用get()或set(T value)時才執行在,而且也是爲了讓子類覆蓋而設計的。
使用案例:
Demo①:ThreadLocal 是一個泛型類,保證可以接受任何類型的對象。
public class ThreadLocalDemo {
private static ThreadLocal<Index> index = new ThreadLocal(){
@Override
protected Object initialValue() {
return new Index();
}
};
private static class Index{
private int num;
public void incr(){
num++;
}
}
public static void main(String[] args) {
for(int i=0; i<5; i++){
new Thread(() ->{
Index local = index.get();
local.incr();
System.out.println(Thread.currentThread().getName() + " " + index.get().num);
}, "thread_" + i).start();
}
}
}
輸出結果:
thread_1 1
thread_0 1
thread_3 1
thread_4 1
thread_2 1
Demo②:SimpleDateFormat是非線程安全(共享變量calendar訪問沒有做到線程安全),ThreadLocal 可以確保每個線程都可以得到單獨的一個 SimpleDateFormat 的對象,那麼自然也就不存在競爭問題了。
ThreadLocal工作原理
ThreadLocal內部維護的是一個類似Map的ThreadLocalMap數據結構,而每個Thread類,都有一個ThreadLocalMap成員變量。ThreadLocalMap將線程本地變量(ThreadLocal)作爲key,線程變量的副本作爲value,如圖所示:
實現原理:ThreadLocal底層實現是ThreadLocalMap數據結構,當使用ThreadLocal維護變量時,ThreadLocalMap將線程本地變量(ThreadLocal)作爲key,線程變量的副本作爲value。每個線程去使用共享變量時,實際調用threadLocal的get()方法,獲取當前線程對應的ThreadLocalMap,然後在根據key獲取value值,就實現了線程安全的操作變量副本的值了。
ThreadLocal源碼解析
想要熟悉和理解 Threadlocal 的源碼的話,我建議先思考這麼三個問題:
1、 Threadlocal 爲什麼能實現每個線程能有一個獨立的變量副本;
2、每個線程的變量副本的儲存位置在哪兒;
3、變量副本是如何從共享變量中複製出來的;
首先我們來看① initialValue( ) 方法:返回的是本地線程變量的初始值。返回值爲空的原因很簡單,這個方法就是用來重寫
* @return the initial value for this thread-local
*/
protected T initialValue() {
return null;
}
②get()源碼分析
2.1 get()源碼入口
public T get() {
//獲取當前線程
Thread t = Thread.currentThread();
//獲取當前線程的ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null) {
//如果ThreadLocalMap已經被創建了,那麼通過當前的threadLocal對象作爲key,獲取value
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
//如果ThreadLocalMap還沒有被創建或者在ThreadLocalMap中查找不到此元素
return setInitialValue();
}
2.1.1 ThreadLocalMap沒初始化,ThreadLocalMap爲null時,會調用setInitialValue()方法:
private T setInitialValue() {
//initialValue方法一般會被重寫,返回變量,不重寫的話,直接返回null
T value = initialValue();
Thread t = Thread.currentThread();
//獲取當前線程的ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null)
//ThreadLocalMap已經被創建,那麼直接設置初始值(即保存變量副本),初始值來自initialValue方法
map.set(this, value);
else
//創建ThreadLocalMap
createMap(t, value);
return value;
}
其中,initialValue()方法是由我們重寫的,需要注意的是,返回值必須爲new一個對象,而不是直接返回一個對象引用。因爲如果多個線程都保存同一個引用的副本的話,那他們通過這個引用修改共享變量的值,是相互影響的。我們本來的目的便是爲了獲取共享變量的初始值副本,各個線程對副本的修改不影響變量本身。這就是能實現每個線程能有一個獨立的變量副本原因。
2.1.2 看看createMap是如何創建threadLocalMap的:
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
//創建一個初始容量爲16的Entry數組
table = new Entry[INITIAL_CAPACITY];
//通過threadLocal的threadLocalHashCode來定位在數組中的位置
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
//保存在數組中
table[i] = new Entry(firstKey, firstValue);
//記錄下已用的大小
size = 1;
//設置閾值爲容量的2/3
setThreshold(INITIAL_CAPACITY);
}
2.2 初始化threadLocalMap之後,此線程再次調用get()方法,又做了哪些操作呢
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
//如果定位的元素的key與傳入的key不相等,那麼一直往後找
return getEntryAfterMiss(key, i, e);
}
可以看到是通過map.getEntry(this)去查找元素的,返回Entry。
2.3 如果map.getEntry(this)也找不到元素怎麼辦?回顧前面講的get入口,先判斷是否能根據當前線程獲取threadLocalMap,第一種情況:threadLocalMap爲空,那麼直接新初始化創建一個。第二請情況:threadLocalMap有值,但是map.getEntry(this) 爲空,這個時候就會在初始化方法裏調用map.set(this, value)方法,將當前參數設置進Map。
private T setInitialValue() {
//initialValue方法一般會被重寫,不重寫的話,直接返回null
T value = initialValue();
Thread t = Thread.currentThread();
//獲取當前線程的ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null)
//ThreadLocalMap已經被創建,那麼直接設置初始值(即保存變量副本),初始值來自initialValue方法
map.set(this, value);
else
//創建ThreadLocalMap
createMap(t, value);
return value;
}
private void set(ThreadLocal<?> key, Object value) {
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;
}
}
//如果在table中確實找不到,那麼新建一個
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
//如果沒有元素被清除,且超過閾值,那麼擴容並重新hash定位
rehash();
}
③set()源碼分析
ThreadLocalMap的set方法和get方法很類似
public void set(T value) {
//獲取當前線程
Thread t = Thread.currentThread();
//獲取當前線程的ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
底層源碼工作原理總結
首先使用ThreadLocal<?>維護變量時,重寫initialValue()方法,返回線程本地變量的初始值。然後每個線程去使用共享變量時,實際調用threadLocal的get()方法,獲取當前線程對應的ThreadLocalMap。進入 get函數,先判斷是否能根據當前線程獲取threadLocalMap,第一種情況:threadLocalMap爲空,那麼直接新初始化創建一個。第二請情況:threadLocalMap有值,但是map.getEntry(this) 爲空,這個時候就會在初始化方法裏調用map.set(this, value)方法,將當前參數設置進Map。第三種情況:threadLocalMap有值,map.getEntry(this) 有值 根據key獲取value直接返回,前兩種情況返回初始化value,實現安全訪問。
threadLocal的set()方法,作用:設置當前線程的線程局部變量的值,實現根據當前線程獲取對應的threadLocalMap,獲取到map直接將值set進去,獲取map爲空,直接創建threadLocalMap將值設置進去。
二、ThreadLocal的內存泄露分析
在分析ThreadLocal導致的內存泄露前,需要普及瞭解一下內存泄露、強引用與弱引用以及GC回收機制,這樣才能更好的分析爲什麼ThreadLocal會導致內存泄露呢?更重要的是知道該如何避免這樣情況發生,增強系統的健壯性。
內存泄露
內存泄露是程序在申請內存後,無法釋放已申請的內存空間,一次內存泄露危害可以忽略,但內存泄露堆積後果很嚴重,無論多少內存,遲早會被佔光
通俗的講:內存一直被對象或者變量佔用,導致內存不能被回收
強引用,使用最普遍的引用,一個對象具有強引用,不會被垃圾回收器回收。當內存空間不足,Java虛擬機寧願拋出OutOfMemoryError錯誤,使程序異常終止,也不回收這種對象。
如果想取消強引用和某個對象之間的關聯,可以顯式地將引用賦值爲null,這樣可以使JVM在合適的時間就會回收該對象。
弱引用,JVM進行垃圾回收時,無論內存是否充足,都會回收被弱引用關聯的對象。在java中,用java.lang.ref.WeakReference類來表示。可以在緩存中使用弱引用。
GC回收機制-如何找到需要回收的對象
JVM如何找到需要回收的對象,方式有兩種:
-
引用計數法:每個對象有一個引用計數屬性,新增一個引用時計數加1,引用釋放時計數減1,計數爲0時可以回收,
-
可達性分析法:以根集對象爲起始點進行搜索,如果有對象不可達的話,即沒有引用指向的對象,就是垃圾對象,jvm垃圾回收的時候將會對垃圾對象進行回收。(根集一般包括Java棧中引用的對象,方法區常量池中引用的對象,堆中引用的對象等)
不可達定義:在java中,對象是通過引用使用的,如果在沒有引用指向該對象的情況下,那麼將無從處理或調用該對象,這樣的對象爲不可達。
ThreadLocal的內存泄露分析
由於Thread中包含變量ThreadLocalMap,因此ThreadLocalMap與Thread的生命週期是一樣長,線程一直沒有完成,如果都沒有手動刪除對應key,都會導致內存泄漏。源碼開發也考慮到了這一點
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
但這次發現:Entry是繼承的WeakReference,並且只綁定了ThreadLocal(WeakReference表示弱引用對象)。
使用弱引用可以多一層保障:弱引用ThreadLocal不會內存泄漏,但是value就不同了,它是強引用,對應的value在下一次ThreadLocalMap調用set(),get(),remove()的時候會被清除。
防止內存泄漏最直接的方法就是使用完變量後調用ThreadLocal的remove(),remove()實際是將對象的引用置爲null,這樣一來沒有引用指向這個對象,該對象就會被JVM判定爲垃圾並在GC時回收掉。
三、ThreadLocal在set()時發生哈希衝突怎麼辦嗎
數據是以鍵值對方式存進Entry數組的,在存入時會根據鍵(ThreadLocal)的哈希值,找到它所存放的位置,但這樣有時會出現哈希衝突,至於如何應對哈希衝突
-
如果該位置是空的,那麼直接將鍵值對存儲;
-
若不爲空且兩個鍵相同,那麼新值換舊值;
-
若不爲空且兩鍵不相同,那隻能找下個空位置了。
文章參考:
https://mp.weixin.qq.com/s/I3hLAcA_cbBG25MGfqKFuw
https://mp.weixin.qq.com/s/B2-sZ9rO8xyrag2j-Xaqlg
https://mp.weixin.qq.com/s/BFNIDxWGJJy_NkIdi9Z7uQ