本文已收錄至Github,推薦閱讀 ?? Java隨想錄
微信公眾號:Java隨想錄
CSDN: 碼農BookSea
目錄烈火試真金,逆境試強者,——塞內加
- 什么是ThreadLocal
- ThreadLocal 原理
- set()方法
- get()方法
- remove()方法
- ThreadLocal 的Hash演算法
- ThreadLocal 1.7和1.8的區別
- ThreadLocal 的問題
- ThreadLocal 記憶體泄露問題
- 為什么使用弱參考而不是強參考?
- ThreadLocal 父子執行緒繼承
- ThreadLocal 記憶體泄露問題
什么是ThreadLocal
首先看下ThreadLocal的使用示例:
public class ThreadLocalTest {
private static ThreadLocal<String> threadLocal = new ThreadLocal<>();
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
threadLocal.set("本地變數1");
print("thread1");
System.out.println("執行緒1的本地變數的值為:"+threadLocal.get());
});
Thread thread2 = new Thread(() -> {
threadLocal.set("本地變數2");
print("thread2");
System.out.println("執行緒2的本地變數的值為:"+threadLocal.get());
});
thread1.start();
thread2.start();
}
public static void print(String s){
System.out.println(s+":"+threadLocal.get());
}
執行結果如下
我們從 Thread 類講起,在 Thread 類中有維護兩個 ThreadLocal.ThreadLocalMap 物件,分別是:threadLocals 和inheritableThreadLocals,
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
/*
* InheritableThreadLocal values pertaining to this thread. This map is
* maintained by the InheritableThreadLocal class.
*/
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
初始它們都為 null,只有在呼叫 ThreadLocal 類的 set 或 get 時才創建它們,ThreadLocalMap可以理解為執行緒私有的HashMap,
ThreadLoalMap是ThreadLocal中的一個靜態內部類,類似HashMap的資料結構,但并沒有實作Map介面,
ThreadLoalMap中初始化了一個大小16的Entry陣列,Entry物件用來保存每一個key-value鍵值對,key是ThreadLocal物件,
Entry用來保存資料 ,而且還是繼承的弱參考,在Entry內部使用ThreadLocal作為key,使用我們設定的value作為value,
ThreadLocal 原理
set()方法
當我們呼叫 ThreadLocal 的 set() 方法時實際是呼叫了當前執行緒的 ThreadLocalMap 的 set() 方法,ThreadLocal 的 set() 方法中,會進一步呼叫Thread.currentThread() 獲得當前執行緒物件 ,然后獲取到當前執行緒物件的ThreadLocal,判斷是不是為空,為空就先呼叫creadMap()創建再set(value)創建 ThreadLocalMap 物件并添加變數,不為空就直接set(value) ,
這種保證執行緒安全的方式稱為執行緒封閉,執行緒只能看到自己的ThreadLocal變數,執行緒之間是互相隔離的,
get()方法
其中get()方法用來獲取與當前執行緒關聯的ThreadLocal的值,如果當前執行緒沒有該ThreadLocal的值,則呼叫initialValue函式獲取初始值回傳,所以一般我們使用時需要繼承該函式,給出初始值(不重寫的話默認回傳Null),
主要有以下幾步:
- 獲取當前的Thread物件,通過getMap獲取Thread內的ThreadLocalMap
- 如果map已經存在,以當前的ThreadLocal為鍵,獲取Entry物件,并從從Entry中取出值
- 否則,呼叫setInitialValue進行初始化,
/**
* Returns the value in the current thread's copy of this
* thread-local variable. If the variable has no value for the
* current thread, it is first initialized to the value returned
* by an invocation of the {@link #initialValue} method.
*
* @return the current thread's value of this thread-local
*/
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();
}
我們可以重寫initialValue(),設定初始值,
private static final ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>(){
@Override
protected Integer initialValue() {
return Integer.valueOf(0);
}
}
remove()方法
最后一個需要探究的就是remove方法,它用于在map中移除一個不用的Entry,也是先計算出hash值,若是第一次沒有命中,就回圈直到null,在此程序中也會呼叫expungeStaleEntry清除空key節點,代碼如下:
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
/**
* Remove the entry for key.
*/
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();
expungeStaleEntry(i);
return;
}
}
}
實際上 ThreadLocalMap 中使用的 key 為 ThreadLocal 的弱參考,弱參考的特點是,如果這個物件只存在弱參考,那么在下一次垃圾回收的時候必然會被清理掉,
所以如果 ThreadLocal 沒有被外部強參考的情況下,在垃圾回收的時候會被清理掉的,這樣一來 ThreadLocalMap中使用這個 ThreadLocal 的 key 也會被清理掉,但是,value 是強參考,不會被清理,這樣一來就會出現 key 為 null 的 value,出現記憶體泄漏的問題,
在執行 ThreadLocal 的 set、remove、rehash 等方法時,它都會掃描 key 為 null 的 Entry,如果發現某個 Entry 的 key 為 null,則代表它所對應的 value 也沒有作用了,所以它就會把對應的 value 置為 null,這樣,value 物件就可以被正常回收了,但是假設 ThreadLocal 已經不被使用了,那么實際上 set、remove、rehash 方法也不會被呼叫,與此同時,如果這個執行緒又一直存活、不終止的話,那么剛才的那個呼叫鏈就一直存在,也就導致了 value 的記憶體泄漏,
ThreadLocal 的Hash演算法
ThreadLocalMap類似HashMap,它有自己的Hash演算法,
private final int threadLocalHashCode = nextHashCode();
private static final int HASH_INCREMENT = 0x61c88647;
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
public final int getAndAdd(int delta) {
return unsafe.getAndAddInt(this, valueOffset, delta);
}
HASH_INCREMENT這個數字被稱為斐波那契數 也叫 黃金分割數,帶來的好處就是 hash 分布非常均勻,
每當創建一個ThreadLocal物件,這個ThreadLocal.nextHashCode 這個值就會增長 0x61c88647 ,
講到Hash就會涉及到Hash沖突,跟HashMap通過鏈地址法不同的是,ThreadLocal是通過線性探測法/開放地址法來解決hash沖突,
ThreadLocal 1.7和1.8的區別
ThreadLocal 1.7版本的時候,entry物件的key是Thread,
1.8版本entry的key是ThreadLocal,
1.8版本的好處 :當Thread銷毀的時候,ThreadLocalMap也會隨之銷毀,減少記憶體的使用,因為ThreadLocalMap是在Thread里面的,所以只要Thread消失了,那ThreadLocalMap就不復存在了,
ThreadLocal 的問題
ThreadLocal 記憶體泄露問題
在 ThreadLocalMap 中的 Entry 的 key 是對 ThreadLocal 的 WeakReference 弱參考,而 value 是強參考,當 ThreadLocalMap 的某 ThreadLocal 物件只被弱參考,GC 發生時該物件會被清理,此時 key 為 null,但 value 為強參考不會被清理,此時 value 將訪問不到也不被清理掉就可能會導致記憶體泄漏,
注意建構式里的第一行代碼super(k),這意味著ThreadLocal物件是一個弱參考
/**
* 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 = https://www.cnblogs.com/booksea/p/v;
}
}
因此我們使用完 ThreadLocal 后最好手動呼叫 remove() 方法,但其實在 ThreadLocalMap 的實作中以及考慮到這種情況,因此在呼叫 set()、get()、remove() 方法時,會清理 key 為 null 的記錄,
為什么使用弱參考而不是強參考?
為什么采用了弱參考的實作而不是強參考呢?
注釋上有這么一段話:為了協助處理資料比較大并且生命周期比較長的場景,hash table的條目使用了WeakReference作為key,
所以,弱參考反而是為了解決記憶體存盤問題而專門使用的,
實際上,采用弱參考反而多了一層保障,ThreadLocal被清理后key為null,對應的value在下一次ThreadLocalMap呼叫set、get,就算忘記呼叫 remove 方法,弱參考比強參考可以多一層保障,
所以,記憶體泄露的根本原因是是否手動清除操作,而不是弱參考,
ThreadLocal 父子執行緒繼承
異步場景下無法給子執行緒共享父執行緒的執行緒副本資料,可以通過 InheritableThreadLocal 類解決這個問題,
它的原理就是子執行緒是通過在父執行緒中呼叫 new Thread() 創建的,在 Thread 的構造方法中呼叫了 Thread的init 方法,在 init 方法中父執行緒資料會復制到子執行緒(ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);),
代碼示例:
public class InheritableThreadLocalDemo {
public static void main(String[] args) {
ThreadLocal<String> threadLocal = new ThreadLocal<>();
ThreadLocal<String> inheritableThreadLocal = new InheritableThreadLocal<>();
threadLocal.set("父類資料:threadLocal");
inheritableThreadLocal.set("父類資料:inheritableThreadLocal");
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("子執行緒獲取父類threadLocal資料:" + threadLocal.get());
System.out.println("子執行緒獲取父類inheritableThreadLocal資料:" +inheritableThreadLocal.get());
}
}).start();
}
}
但是我們做異步處理都是使用執行緒池,執行緒池會復用執行緒會導致問題出現,我們可以使用阿里巴巴的TTL解決這個問題,
https://github.com/alibaba/transmittable-thread-local
如果本篇博客有任何錯誤和建議,歡迎給我留言指正,文章持續更新,可以關注公眾號第一時間閱讀,
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/543855.html
標籤:Java
上一篇:大數處理方案
