簡述ThreadLocal 使用和原理
線上程的調用中可以通過ThreadLocal進行的參數傳輸,減少方法中的參數一層層的嵌套下去,而且它是線程安全的,通過使用ThreadLocal 的方式可以在代碼和多線程的情況下處理很多線程安全的問題。
那它是怎樣做到線上程裡面裡面做到線程安全的呢?
那它是通過怎樣的格式儲存資料的呢?
存在多資料的時候是否會導緻記憶體洩露呢?
接下來将探讨下ThreadLocal底層的實作
簡單 main 方法示例:
public class TreadLocalDemo {
private static ThreadLocal<String> threadLocal1 = new ThreadLocal<>();
private static ThreadLocal<String> threadLocal2 = new ThreadLocal<>();
public static void main(String[] args) throws InterruptedException {
new Thread(()->{
threadLocal1.set("11111");
threadLocal2.set("22222");
System.out.println(threadLocal1.get());
System.out.println(threadLocal2.get());
threadLocal2.remove();
}).start();
}
}
得到結果:

這裡可以看到 通過set方法就可以 在同一個線程下面每個地方可以通過擷取到threadLocal 的引用就可以通過Get 方法擷取到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);
}
這裡可以看到通過擷取到目前Thread 然後擷取之前是否已經存在了ThreadLocalMap 對象;
如果目前線程沒有ThreadLocalMap 就會建立一個,并且把引用設定會Thread 對象裡面,建立ThreadLocalMap 時初始化一個預設值為16的 Entry[] ,并且Entry對應的key就是目前ThreadLocal的引用,而且是個弱應用(後面會描述他們的關系),value就是set的值。
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
/**
* 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) {
// 建立一個預設長度等于16的數組
table = new Entry[INITIAL_CAPACITY];
// 通過目前ThreadLocal hashcode值 和 目前的容量 取 &
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
// 設定值
table[i] = new Entry(firstKey, firstValue);
size = 1;
// 是這裡計算下一次擴充臨界值長度
setThreshold(INITIAL_CAPACITY);
}
關于 firstKey.threadLocalHashCode 計算方式可以參考: https://zhuanlan.zhihu.com/p/40515974
如果目前對象已經存在ThreadLocalMap的話
/**
* 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();
}
這裡代碼的大體已經就是通過計算出目前threadLocal的索引值 然後把值設定到對應的ThreadLocalMap的Entry對應的數組中。
這裡描述ThreadLocal 、ThreadLocalMap、Entry[]、Entry 它們的關系:
一個Thread,裡面隻有一個THreadLocalMap,在ThreadLocalMap 裡面是以會有一個Entry[],而且在建立Entry對象的時候 Key是threadLcoal、value 是設定的值
上圖可以說明:
如果我們目前Thread 執行完成後就會斷開強引用,隻有一個弱引用,這樣弱引用對象來說就會被垃圾回收機制給回收,防止記憶體溢出。
難道這樣就不會産生記憶體溢出嗎?假如設定 threadLocal = null ,而且目前Thread還存活這時候。Entry對象裡面的Key是弱應用他會不會被回收 ? 不會(因為它本身其實是線程本身強引用,隻有當不存線上程的強引用之後,這個weakreference才會在GC的時候被端),但是這時候Entry 對象裡面的 key = null 這樣下來不一樣會存在不斷的記憶體占用嗎? 一樣有可能導緻記憶體的溢出。
當然上面說的設計者也想到了,是以分别在執行set、get、remove的方式裡面都會嘗試或者直接去檢查key = null 的值去移除。(這裡并不是所有的set、get操作都會去執行,remove 是先設定成null然後去移除 可以去看看源碼裡面的這個方法 expungeStaleEntry )
也許你留意到我上面的main方法的代碼裡面的ThreadLocal是一個靜态的對象,如果你把他加上一個final 的話 ,上面是不是就成為一個僞命題了。。。具體實作Thread 寫法很多。但是建議最後還是我們通過手動remove() 方法進行移除。
可能有人會問為什麼需要把存儲對應格式設計成數組,那你可以看看我一開始的main方法,因為一個Thread裡面可以持有一個或者多個以上的ThreadLcoal的引用,這樣的話就需要數組來進行存儲了。
舉例使用場景:
1、spring使用ThreadLocal解決線程安全問題我們知道在一般情況下,隻有無狀态的Bean才可以在多線程環境下共享,在Spring中,絕大部分Bean都可以聲明為singleton作用域。就是因為Spring對一些Bean(如 RequestContextHolder、 TransactionSynchronizationManager、 LocaleContextHolder等)中非線程安全狀态采用ThreadLocal進行處理讓它們也成為線程安全的狀态。 2、PageHelper找那個startPage就是通過ThreadLocal來進行儲存目前分頁資訊。
這裡隻是本人對ThreadLocal一些簡單的見解,如果有錯誤之處請諒解,望指出,謝謝!!!