深入理解ThreadLocal
在java的多線程模塊中,ThreadLocal是經常被提問到的一個知識點 ,因此只有理解透徹了,不管怎么問,都能游刃有余。
本文主要從以下幾個角度來分析理解
- ThreadLocal是什么
- ThreadLocal怎么用
- ThreadLocal源碼分析
- ThreadLocal內存泄漏問題
PS:以下源碼均基于jdk8。
1. ThreadLocal是什么?
從名字我們就可以看到ThreadLocal 叫做本地線程變量,意思是說,ThreadLocal 中填充的的是當前線程的變量,該變量對其他線程而言是封閉且隔離的,ThreadLocal 為變量在每個線程中創建了一個副本,這樣每個線程都可以訪問自己內部的副本變量。
從字面意思很容易理解,但是實際角度就沒那么容易了,作為一個面試常問的點,使用場景也是很豐富。
- 在進行對象跨層傳遞的時候,使用ThreadLocal可以避免多次傳遞,打破層次間的約束。
- 線程間數據隔離
- 進行事務操作,用于存儲線程事務信息。
- 數據庫連接,Session會話管理。
現在相信你已經對ThreadLocal有一個大致的認識了,下面我們看看如何用?
2. ThreadLocal怎么用?
下面讓我們來看一個例子:
import java.util.stream.IntStream; public class ThreadLocalTest { public static void main(String[] args) { ThreadLocal<String> local = new ThreadLocal<>(); IntStream.range(0, 10).forEach(i -> new Thread(() -> { local.set(Thread.currentThread().getName() + ":" + i); System.out.println("線程:" + Thread.currentThread().getName() + ",local:" + local.get()); }).start()); } }

從結果可以看到,每一個線程都有自己的local 值,這就是TheadLocal的基本使用 。
3. ThreadLocal工作原理
下面我們從源碼的角度來分析一下,ThreadLocal的工作原理。
3.1、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(); //獲取線程中變量 ThreadLocal.ThreadLocalMap ThreadLocalMap map = getMap(t); //如果不為空, if (map != null) map.set(this, value); else //如果為空,初始化該線程對象的map變量,其中key 為當前的threadlocal 變量 createMap(t, value); } /** * Create the map associated with a ThreadLocal. Overridden in * InheritableThreadLocal. * * @param t the current thread * @param firstValue value for the initial entry of the map */ //初始化線程內部變量 threadLocals ,key 為當前 threadlocal 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) { table = new Entry[INITIAL_CAPACITY]; int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); table[i] = new Entry(firstKey, firstValue); size = 1; setThreshold(INITIAL_CAPACITY); } static class Entry extends WeakReference<ThreadLocal<?>> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } }
匯總下,ThreadLocalMap 為 ThreadLocal 的一個靜態內部類,里面定義了Entry 來保存數據。而且是繼承的弱引用。在Entry內部使用ThreadLocal作為key,使用我們設置的value作為value。
對于每個線程內部有個ThreadLocal.ThreadLocalMap 變量,存取值的時候,也是從這個容器中來獲取。
3.2、get方法
/** * 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(); }
通過上面的分析,相信你對該方法已經有所理解了,首先獲取當前線程,然后通過key threadlocal 獲取 設置的value 。
4. 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 = v; } }
注釋說的很清楚了,Note that null keys (i.e. entry.get()* == null)
如果 key threadlocal 為 null 了,這個 entry 就可以清除了。
ThreadLocal是一個弱引用,當為null時,會被當成垃圾回收 。

重點來了,突然我們ThreadLocal是null了,也就是要被垃圾回收器回收了,但是此時我們的ThreadLocalMap(thread 的內部屬性)生命周期和Thread的一樣,它不會回收,這時候就出現了一個現象。那就是ThreadLocalMap的key沒了,但是value還在,這就造成了內存泄漏。
解決辦法:使用完ThreadLocal后,執行remove操作,避免出現內存溢出情況。
所以 如同 lock 的操作 最后要執行解鎖操作一樣,ThreadLocal使用完畢一定記得執行remove 方法,清除當前線程的數值。
如果不remove 當前線程對應的VALUE ,就會一直存在這個值。
使用了線程池,可以達到“線程復用”的效果。但是歸還線程之前記得清除ThreadLocalMap,要不然再取出該線程的時候,ThreadLocal變量還會存在。這就不僅僅是內存泄露的問題了,整個業務邏輯都可能會出錯。
5. 為什么key使用弱引用?
如果使用強引用,當ThreadLocal 對象的引用(強引用)被回收了,ThreadLocalMap本身依然還持有ThreadLocal的強引用,如果沒有手動刪除這個key ,則ThreadLocal不會被回收,所以只要當前線程不消亡,ThreadLocalMap引用的那些對象就不會被回收, 可以認為這導致Entry內存泄漏。
附:強引用-軟引用-弱引用
- 強引用:普通的引用,強引用指向的對象不會被回收;
- 軟引用:僅有軟引用指向的對象,只有發生gc且內存不足,才會被回收;
- 弱引用:僅有弱引用指向的對象,只要發生gc就會被回收。
浙公網安備 33010602011771號