硬核剖析ThreadLocal源碼,面試官看了直呼內行( 三 )

5. ThreadLocal使用注意事項使用ThreadLocal結束,一定要調用remove方法,清理掉threadLocal數據 。具體流程類似下面這樣:
/** * @author 一燈架構 * @apiNote ThreadLocal示例 **/public class ThreadLocalDemo {// 1. 創建ThreadLocalstatic ThreadLocal<User> threadLocal = new ThreadLocal<>();public void method() {try {User user = getUser();// 2. 給threadLocal賦值threadLocal.set(user);// 3. 執行其他業務邏輯doSomething();} finally {// 4. 清理threadLocal數據threadLocal.remove();}}}如果忘了調用remove方法,可能會導致兩個嚴重的問題:

  1. 導致內存溢出
    如果線程的生命周期很長,一直往ThreadLocal中放數據,卻沒有刪除,最終產生OOM
  2. 導致數據錯亂
    如果使用了線程池,一個線程執行完任務后并不會被銷毀,會繼續執行下一個任務,導致下個任務訪問到了上個任務的數據 。
6. 常見面試題剖析看完了ThreadLocal源碼,再回答幾道面試題,檢驗一下學習成果怎么樣 。
6.1 ThreadLocal是怎么保證數據安全性的?ThreadLocal底層使用的ThreadLocalMap存儲數據 , 而ThreadLocalMap是線程Thread的私有變量,不同線程之間數據隔離,所以即使ThreadLocal的set、get、remove方法沒有加鎖,也能保證線程安全 。
硬核剖析ThreadLocal源碼,面試官看了直呼內行

文章插圖
6.2 ThreadLocal底層為什么使用數組?而不是一個對象?因為在一個線程中可以創建多個ThreadLocal實例對象,所以要用數組存儲,而不是用一個對象 。
6.3 ThreadLocal是怎么解決哈希沖突的?ThreadLocal使用的線性探測法法解決哈希沖突,線性探測法法具體賦值流程如下:
  1. 通過key的hashcode找到數組下標
  2. 如果數組下標位置是空或者等于當前ThreadLocal對象,直接覆蓋值結束
  3. 如果不是空,就繼續向下遍歷,遍歷到數組結尾后 , 再從頭開始遍歷,直到找到數組為空的位置,在此位置賦值結束
6.4 ThreadLocal為什么要用線性探測法解決哈希沖突?我們都知道HashMap采用的是鏈地址法(也叫拉鏈法)解決哈希沖突,為什么ThreadLocal要用線性探測法解決哈希沖突?而不用鏈地址法呢?
我的猜想是可能是創作者偷懶、嫌麻煩 , 或者是ThreadLocal使用量較少,出現哈希沖突概率較低,不想那么麻煩 。
使用鏈地址法需要引入鏈表和紅黑樹兩種數據結構 , 實現更復雜 。而線性探測法沒有引入任何額外的數據結構,直接不斷遍歷數組 。
結果就是,如果一個線程中使用很多個ThreadLocal,發生哈希沖突后,ThreadLocal的get、set性能急劇下降 。
線性探測法相比鏈地址法優缺點都很明顯:
優點: 實現簡單,無需引入額外的數據結構 。
缺點: 發生哈希沖突后,ThreadLocal的get、set性能急劇下降 。
6.5 ThreadLocalMap的key為什么要設計成弱引用?先說一下弱引用的特點:
弱引用的對象擁有更短暫的生命周期 , 在垃圾回收器線程掃描它所管轄的內存區域的過程中,一旦發現了只具有弱引用的對象,不管當前內存空間足夠與否 , 都會回收它的內存 。不過,由于垃圾回收器是一個優先級很低的線程,因此不一定會很快發現那些只具有弱引用的對象 。
ThreadLocalMap的key設計成弱引用后 , 會不會我們正在使用,就被GC回收了?
這個是不會的,因為我們一直在強引用著ThreadLocal實例對象 。
/** * @author 一燈架構 * @apiNote ThreadLocal示例 **/public class ThreadLocalDemo {// 1. 創建ThreadLocalstatic ThreadLocal<String> threadLocal = new ThreadLocal<>();public static void main(String[] args) {// 2. 給ThreadLocal賦值threadLocal.set("關注公眾號:一燈架構");// 3. 從ThreadLocal中取值String result = threadLocal.get();// 手動觸發GCSystem.gc();System.out.println(result); // 輸出 關注公眾號:一燈架構}}由上面代碼中得知 , 如果我們一直在使用threadLocal,觸發GC后,并不會threadLocal實例對象 。
ThreadLocalMap的key設計成弱引用的目的就是:
防止我們在使用完ThreadLocal后,忘了調用remove方法刪除數據,導致數組中ThreadLocal數據一直不被回收 。
/** * @author 一燈架構 * @apiNote ThreadLocal示例 **/public class ThreadLocalDemo {// 1. 創建ThreadLocalstatic ThreadLocal<String> threadLocal = new ThreadLocal<>();public static void main(String[] args) {// 2. 給ThreadLocal賦值threadLocal.set("關注公眾號:一燈架構");// 3. 使用完threadLocal,設置成null,模仿生命周期結束threadLocal = null;// 觸發GC,這時候ThreadLocalMap的key就會被回收 , 但是value還沒有被回收 。// 只有等到下次執行get、set方法遍歷數組,遍歷到這個位置 , 才會刪除這個無效的valueSystem.gc();}}

推薦閱讀