前言 ThreadLocal可以用來存儲線程的本地數據,做到線程數據的隔離 ThreadLocal的使用不當可能會導致記憶體泄漏,排查記憶體泄漏的問題,不僅需要熟悉JVM、利用好各種分析工具還耗費人工 如果能明白其原理並正確使用,就不會導致各種意外發生 本文將從使用場景、實現原理、記憶體泄漏、設計思想等層 ...
前言
ThreadLocal可以用來存儲線程的本地數據,做到線程數據的隔離
ThreadLocal的使用不當可能會導致記憶體泄漏,排查記憶體泄漏的問題,不僅需要熟悉JVM、利用好各種分析工具還耗費人工
如果能明白其原理並正確使用,就不會導致各種意外發生
本文將從使用場景、實現原理、記憶體泄漏、設計思想等層面分析ThreadLocal,並順帶聊聊InheritableThreadLocal
ThreadLocal使用場景
什麼是上下文?
比如線程處理一個請求,請求會經過MVC流程,由於流程很長,會經歷很多方法,這些方法就可以叫上下文
ThreadLocal作用在上下文中存儲常用的數據、存儲會話信息、存儲線程本地變數等
比如使用攔截器在請求處理前,通過請求中的token獲取登錄用戶信息,將用戶信息存儲在ThreadLocal中,方便後續處理請求時從ThreadLocal中直接獲取用戶信息
如果線程會重覆利用,為了避免數據錯亂,使用完(攔截器處理後)應該刪除該數據
ThreadLocal 常用的方法有:set()
、get()
、remove()
分別對應存儲、獲取和刪除
可以將ThreadLocal放在工具類中方便使用
public class ContextUtils {
public static final ThreadLocal<UserInfo> USER_INFO_THREAD_LOCAL = new ThreadLocal();
}
攔截器偽代碼
//執行前 存儲
public boolean postHandle(HttpServletRequest request) {
//解析token獲取用戶信息
String token = request.getHeader("token");
UserInfo userInfo = parseToken(token);
//存入
ContextUtils.USER_INFO_THREAD_LOCAL.set(userInfo);
return true;
}
//執行後 刪除
public void postHandle(HttpServletRequest request) {
ContextUtils.USER_INFO_THREAD_LOCAL.remove();
}
使用時
//提交訂單
public void orderSubmit(){
//獲取用戶信息
UserInfo userInfo = ContextUtils.USER_INFO_THREAD_LOCAL.get();
//下單
submit(userInfo);
//刪除購物車
removeCard(userInfo);
}
為了更好的使用ThreadLocal,我們應該瞭解其實現原理,避免使用不當造成意外發生
ThreadLocalMap
Thread 線程中有兩個欄位存儲ThreadLocal的內部類ThreadLocalMap
public class Thread implements Runnable {
ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
}
threadLocals用於實現ThreadLocal
inheritableThreadLocals 用於實現InheritableThreadLocal (可繼承的ThreadLocal 後文再聊)
ThreadLocalMap 的實現是哈希表,其內部類Entry是哈希表的節點,由Entry數組實現哈希表 ThreadLocalMap
public class ThreadLocal<T> {
//,,,
static class ThreadLocalMap {
//...
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
}
}
節點構造中的Key是ThreadLocal,Value是需要存儲的值
同時節點繼承弱引用,通過泛型和構造可以知道它將ThreadLocal設置為弱引用
不理解弱引用的同學可以查看這篇文章:深入淺出JVM(十四)之記憶體溢出、泄漏與引用 )
set
在存儲數據的方法中
獲取ThreadLocalMap,如果沒有則初始化ThreadLocalMap(懶載入)
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);
}
}
createMap
創建ThreadLocalMap賦值給當前線程的threadLocals
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
創建ThreadLocalMap,初始化長度為16的數組
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
//初始化數組 16
table = new Entry[INITIAL_CAPACITY];
//獲取下標
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
//構建節點
table[i] = new Entry(firstKey, firstValue);
//設置大小
size = 1;
//設置負載因數
setThreshold(INITIAL_CAPACITY);
}
ThreadLocalMap.set
通過哈希獲取下標,當發生哈希衝突時,遍歷哈希表(不再使用鏈地址法)直到位置上沒有節點再進行構建
遍歷期間如果有節點,則根據節點取出key進行比較,如果是則是覆蓋;如果節點沒有key說明該節點的ThreadLocal被回收(已過期),為了防止記憶體泄漏會清理節點
最後會檢查其他位置有沒有已過期的節點進行清理,並檢查擴容
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)]) {
//獲取key
ThreadLocal<?> k = e.get();
//key如果存在則覆蓋
if (k == key) {
e.value = value;
return;
}
//如果key不存在 說明該ThreadLocal以及不再使用(GC回收),需要清理防止記憶體泄漏
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
//構建節點
tab[i] = new Entry(key, value);
//計數
int sz = ++size;
//清理其他過期的槽,如果滿足條件進行擴容
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
獲取哈希值時,使用哈希值自增的原子類獲取,步長則是每次自增的數量(也許是經過研究、測試的,儘量減少哈希衝突)
//獲取哈希值
private final int threadLocalHashCode = nextHashCode();
//哈希值自增器
private static AtomicInteger nextHashCode =
new AtomicInteger();
//增長步長
private static final int HASH_INCREMENT = 0x61c88647;
//獲取哈希值
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
nextIndex是獲取下一個下標,超出上限時回到0
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
get
在獲取數據時
獲取當前線程的ThreadLocalMap,如果為空則初始化,否則獲取節點
public T get() {
//獲取當前線程
Thread t = Thread.currentThread();
//獲取線程的ThreadLocalMap
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();
}
在獲取節點時,先根據哈希值獲取到下標,再查看節點,比較key;如果匹配不上則說明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是否過期,如果過期就清理
實際上ThreadLocal使用不當是會造成記憶體泄漏的
設計者為了避免使用不當導致的記憶體泄漏,在常用方法中儘量清理這些過期的ThreadLocal
前文說過節點繼承弱引用,在構造中設置key為弱引用(也就是ThreadLocal)
當ThreadLocal在任何地方都不被使用時,下次GC會將節點的key設置為空
如果value也不再使用,但是由於節點Entry(null,value)存在,就無法回收value,導致出現記憶體泄漏
因此使用完數據後,儘量使用remove進行刪除
並且設計者在set、get、remove等常用方法中都會檢查key為空的節點並刪除,避免記憶體泄漏
設計思想
為什麼要把entry中的key,也就是ThreadLocal設置成弱引用?
我們先想象一個場景:線程在我們的服務中經常重覆利用,而在某些場景下ThreadLocal並不長期使用
如果節點entry 的key、value都是強引用,一但不再使用ThreadLocal,那麼這個ThreadLocal還作為強引用存儲在節點中,那麼就無法回收,相當於發生記憶體泄漏
把ThreadLocal設置為弱引用後,這種場景下如果value也不再使用依舊會發生記憶體泄漏,因此在set、get、remove方法中都會區檢查刪除key為空的節點,避免記憶體泄漏
既然value可能無法回收,為什麼不把value也設置成弱引用?
由於value存儲的是線程隔離的數據,如果將value設置成弱引用,當外層也不使用value對應的對象時,它就沒有強引用了,再下次gc被回收,導致數據丟失
InheritableThreadLocal
InheritableThreadLocal 繼承 ThreadLocal 用於父子線程間的線程變數傳遞
public void testInheritableThreadLocal(){
InheritableThreadLocal<String> itl = new InheritableThreadLocal<>();
itl.set("main");
new Thread(()->{
//main
System.out.println(itl.get());
}).start();
}
前文說過線程中另一個ThreadLocalMap就是用於InheritableThreadLocal 的
在創建線程時,如果父線程中inheritableThreadLocals 不為空 則傳遞
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals) {
//....
//如果父線程中inheritableThreadLocals 不為空 則傳遞
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
}
總結
ThreadLocal 用於隔離線程間的數據,可以存儲數據作用在上下文中,由於線程可能重覆利用,使用後需要刪除,避免出現數據混亂
Thread線程中存儲ThreadLocalMap,ThreadLocalMap是一個使用開放定址法解決哈希衝突的哈希表,其中節點存儲Key是ThreadLocal,Value存儲的是線程要存儲數據
節點繼承弱引用,並設置ThreadLocal為弱引用,這就導致當ThreadLocal不再使用時,下次GC會將其回收,此時Key為空,如果Value也不再使用,但是節點未刪除就會導致value被使用,從而導致記憶體泄漏
在ThreadLocal的set、get、remove等常用方法中,遍曆數組的同時還回去將過期的節點(key為空)進行刪除,避免記憶體泄漏
如果將ThreadLocal設置成強引用,當ThreadLocal不再使用時會發生記憶體泄漏;將ThreadLocal設置成弱引用時,雖然也可能發生記憶體泄漏,但可以在常用方法中檢查並清理這些數據;如果將value設置成弱引用,當外層不使用value時會發生數據丟失
InheritableThreadLocal繼承ThreadLocal ,用於父子線程間的ThreadLocal數據傳遞
最後(不要白嫖,一鍵三連求求拉~)
本篇文章被收入專欄 由點到線,由線到面,深入淺出構建Java併發編程知識體系,感興趣的同學可以持續關註喔
本篇文章筆記以及案例被收入 gitee-StudyJava、 github-StudyJava 感興趣的同學可以stat下持續關註喔~
案例地址:
Gitee-JavaConcurrentProgramming/src/main/java/G_ThreadLocal
Github-JavaConcurrentProgramming/src/main/java/G_ThreadLocal
有什麼問題可以在評論區交流,如果覺得菜菜寫的不錯,可以點贊、關註、收藏支持一下~
關註菜菜,分享更多乾貨,公眾號:菜菜的後端私房菜
本文由博客一文多發平臺 OpenWrite 發佈!