1. 程式人生 > >ThreadLocal深度解析

ThreadLocal深度解析

OS jdk1.8 自帶 使用 存在 線程 都是 ren 但是

本文基於jdk1.8.0_66寫成

0. ThreadLocal簡介

ThreadLocal可以提供線程內的局部對象,合理的使用可以避免線程沖突的問題
比方說SimpleDateFormat是線程不安全的,但是如果用ThreadLocal給每個線程分配一個SimpleDateFormat對象,我們就可以安全的使用了

為了便於理解,我們可以將ThreadLocal想象成一個Map,key為當前線程,value為存入的值
threadLocal.get() 等效於 map.get(Thread.currentThread())
threadLocal隱式的幫我們完成了獲取當前線程的操作,使用起來更為方便

1. naive的想法

如同簡介裏所說,ThreadLocal最直接的設計思路是:
在ThreadLocal內部維護一個Map,key為當前線程,value為存入的值
ThreadLocal.set(obj)等效於Map.set(Thread.currentThread(), obj)
ThreadLocal.get()等效於Map.get(Thread.currentThread())

這個想法的優點是實現簡單,但是問題也很多:

a. 一般來說,一個ThreadLocal會與多個Thread關聯,而一個Thread只會與少數的ThreadLocal關聯。所以從ThreadLocal去尋找關聯的Thread,開銷比從Thread尋找關聯的ThreadLocal要大。
b. 如果一個Thread死掉了,為了防止內存泄漏,所有ThreadLocal中與這個Thread關聯的value都要被釋放,這個過程是手動的,不優雅,而且開銷較大。

2. JDK1.8中的想法

每個Thread各自維護一個名為threadLocals的變量,其類型為ThreadLocal.ThreadLocalMap
這個Map的key是ThreadLocal,value是關聯的值
在調用ThreadLocal.get/set時,先用Thread.currentThread()獲得當前Thread,然後找到當前Thread的threadLocals域,再以當前ThreadLocal為key找到對應的value並返回。

這樣做好處很多:

a. 一般來說,一個Thread只會與少數的幾個ThreadLocal關聯,那麽從Thread去尋找對應的ThreadLocal開銷是很小的
b. 如果一個Thread死掉了,那麽它所關聯的threadLocals也會被自動釋放,在很大程度上避免了內存泄漏的問題


3. Netty中的進一步改進

Netty自定義了一個名為FastThreadLocal的東西
大概想法:
原版ThreadLocal中,在ThreadLocal.ThreadLocalMap中查找時,采用的是線性探測法,發生哈希碰撞時會導致查詢變慢
為了避免這一問題,Netty為每個FastThreadLocal都設置了獨一無二的編號,Thread可以直接根據這個編號尋址
這樣做絕對不會有哈希碰撞,但是占用的空間也相應變大了,也就是空間換時間的套路。


4. ThreadLocal與內存泄漏

ThreadLocal有個很微妙的地方在於,它在某些場景下,還是會發生內存泄漏

如果我們在函數裏定義了一個局部的ThreadLocal變量,主線程往裏面set了一個很大的對象Huge後就退出這個函數該幹嘛幹嘛去了
現在這個ThreadLocal連帶著Huge都是垃圾了,但是gc能回收他們嗎?

按照一般的思路,ThreadLocal和Huge都會被Thread自帶的threadLocals引用,所以都不會被回收。
但是JDK的作者比我機智很多了,他們把ThreadLocal.ThreadLocalMap.Entry弄成了弱引用(WeakReference),也就是說沒有引用的ThreadLocal對象是會在full gc中被回收的。
但是問題依然存在:雖然ThreadLocal被回收了,但是它關聯的Huge對象卻還在,這可如何是好?

JDK的作者此時又玩了個騷操作,在對ThreadLocal做set操作時,會去檢查ThreadLocal.ThreadLocalMap的底層數組,如果發現某個key是null了(ThreadLocal被gc了),它會把對應的value也設為null,這樣Huge對象就可以被釋放了。

但是為了性能考慮,這個檢查操作不會遍歷整個底層數組,而是每次只掃描一小段,所以在某些特定的場景下,還是會發生內存泄漏的。

ThreadLocal深度解析