一、寫在開頭
? ? 今天和一個之前研二的學妹聊天,聊及她上周面試字節的情況,著實感受到了Java后端現在找工作的壓力啊,記得在18,19年的時候,研究生計算機專業的學生,背背八股文找個Java開發工作毫無問題,但現在即便你是應屆生,問的考題也非常的深入和細節了,只會背八股,沒有一定的代碼量和項目積累,根本找不到像樣的工作,具體聊天內容如下:
既然大廠的面試都拷問到ThreadLocal了,那今天build哥就花點時間也來溫習一下這個知識點吧,盡可能整理的細致一點!🤓🤓
二、ThreadLocal簡介
2.1 ThreadLocal的作用
處理并發編程的時候,其核心問題是當多個線程去訪問共享變量時,因為順序、資源分配等原因帶來了數據的不準確,我們叫這種情況為線程不安全,為了解決線程安全問題,在Java中可以采用Lock、 synchronzed關鍵字等方式,但這種方式對于沒有持有鎖的線程來說會阻塞,這樣以來在時間性能上就有所損失。
為了解決這個問題,Java的lang包中誕生出了一個類,名為 ThreadLocal
,見名知意,它被視為線程的“本地變量”
,主要用來存儲各線程的私有數據,當多個線程訪問同一個ThreadLocal變量時,實際上它們訪問的是各自線程本地存儲的副本,而不是共享變量本身。因此,每個線程都可以獨立地修改自己的副本,而不會影響到其他線程。這種以空間換時間的方式,可以大大的提升處理時間。
2.2 ThreadLocal的使用案例
上面了解了它的特性后,我們來寫一個小demo感受一下ThreadLocal的使用。
public class TestService implements Runnable{// SimpleDateFormat 不是線程安全的,所以每個線程都要有自己獨立的副本//共享變量private static final ThreadLocal<SimpleDateFormat> formatter = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyyMMdd"));public static void main(String[] args) throws InterruptedException {TestService obj = new TestService();//循環創建5個線程for(int i=0 ; i<5; i++){Thread t = new Thread(obj, ""+i);Thread.sleep(new Random().nextInt(1000));t.start();}}@Overridepublic void run() {System.out.println("Thread:"+Thread.currentThread().getName()+" default Formatter = "+formatter.get().toPattern());try {Thread.sleep(new Random().nextInt(1000));} catch (InterruptedException e) {e.printStackTrace();}//formatter pattern is changed here by thread, but it won't reflect to other threads//設置副本的值formatter.set(new SimpleDateFormat());System.out.println("Thread:"+Thread.currentThread().getName()+" formatter = "+formatter.get().toPattern());}
}
輸出:
Thread:0 default Formatter = yyyyMMdd
Thread:1 default Formatter = yyyyMMdd
Thread:2 default Formatter = yyyyMMdd
Thread:1 formatter = yy-M-d ah:mm
Thread:0 formatter = yy-M-d ah:mm
Thread:3 default Formatter = yyyyMMdd
Thread:2 formatter = yy-M-d ah:mm
Thread:3 formatter = yy-M-d ah:mm
Thread:4 default Formatter = yyyyMMdd
Thread:4 formatter = yy-M-d ah:mm
從輸出中可以看出,雖然 Thread-0 已經改變了 formatter 的值,但 Thread-1 默認格式化值與初始化值相同并沒有被修改,其他線程也一樣,這說明每個線程獲取ThreadLocal變量值的時候,確訪問的時線程本地的副本值。
三、ThreadLocal的實現原理
我們從Thread源碼入手,一步步的跟進,去探索ThreadLocal的實現原理。首先,在Thread的源碼中,我們看到了這樣的兩句定義語句:
public class Thread implements Runnable {//......//與此線程有關的ThreadLocal值。由ThreadLocal類維護ThreadLocal.ThreadLocalMap threadLocals = null;//與此線程有關的InheritableThreadLocal值。由InheritableThreadLocal類維護ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;//......
}
threadLocals 、inheritableThreadLocals 都是ThreadLocalMap變量,而這個Map我們可以看作是ThreadLocal的定制化HashMap,用來存儲線程本地變量的容器,是一個靜態內部類,而這兩個變量的值初始為null,只有當前線程調用 ThreadLocal 類的 set或get方法時才創建它們,那我們繼續去看set/get方法。
【set方法解析】
public void set(T value) {//1. 獲取當前線程實例對象Thread t = Thread.currentThread();//2. 通過當前線程實例獲取到ThreadLocalMap對象ThreadLocalMap map = getMap(t);if (map != null)//3. 如果Map不為null,則以當前ThreadLocal實例為key,值為value進行存入map.set(this, value);else//4.map為null,則新建ThreadLocalMap并存入valuecreateMap(t, value);
}
在ThreadLocal的set方法中通過getMap()方法去獲取當前線程的ThreadLocalMap對象,并對獲取到的map進行判斷,我們跟如到getMap方法中去,發現其實里面返回的是初始化定義的threadLocals變量。
ThreadLocalMap getMap(Thread t) {return t.threadLocals;
}
在threadLocals沒有被調用初始化方法重新賦值的時候,它為null(不為null時,直接set進行賦值,當前ThreadLocal實例為key,值為value),set方法中會去調用createMap(t,value)進行處理,我們繼續跟入這個方法的源碼去看看:
void createMap(Thread t, T firstValue) {t.threadLocals = new ThreadLocalMap(this, firstValue);
}
我們可以看到,在這個方法內部,會去新構造一個ThreadLocalMap的實例,并將value值初始化進去,并賦給threadLocals。
看完了set方法的底層實現我們知道:
- 最終變量存儲的位置在ThreadLocalMap里,ThreadLocal可以視為這個Map的封裝;
- 無論如何最終threadLocals存儲的數據都是以線程為key,對應的局部變量為值得映射表;
- 因為映射表的原因,確保了每個線程的局部變量都時獨立的。
【get方法解析】
看完了set的源碼,我們繼續來看看get方法的底層實現吧,既然有存(set)就有取(get),get 方法提供的就是獲取當前線程中 ThreadLocal 的變量值的功能!
public T get() {//1. 獲取當前線程的實例對象Thread t = Thread.currentThread();//2. 獲取當前線程的ThreadLocalMapThreadLocalMap map = getMap(t);if (map != null) {//3. 獲取map中當前ThreadLocal實例為key的值的entryThreadLocalMap.Entry e = map.getEntry(this);if (e != null) {@SuppressWarnings("unchecked")//4. 當前entitiy不為null的話,就返回相應的值valueT result = (T)e.value;return result;}}//5. 若map為null或者entry為null的話通過該方法初始化,并返回該方法返回的valuereturn setInitialValue();
}
我們上面提到了線程的變量值是和線程的ThreadLocal有映射關系的,所以這里將當前線程的ThreadLocal作為key去map中獲取值,若map為null或者entry為null的話通過該方法初始化,并返回該方法返回的value,我們去看看setInitialValue的實現:
private T setInitialValue() {T value = initialValue();Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null)map.set(this, value);elsecreateMap(t, value);return value;
}
protected T initialValue() {return null;
}
這個方法里的實現和set幾乎一模一樣,這里調用了一個protected訪問修飾符的方法initialValue(),這個方法可以被子類重寫。
我們在2.2使用案例中寫道的ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyyMMdd"));
這是在Java8中的寫法,等價于:
private static final ThreadLocal<SimpleDateFormat> formatter = new ThreadLocal<SimpleDateFormat>(){@Overrideprotected SimpleDateFormat initialValue(){return new SimpleDateFormat("yyyyMMdd");}
};
setInitialValue 方法的目的是確保每個線程在第一次嘗試訪問其 ThreadLocal 變量時都有一個合適的值。
3.1 ThreadLocalMap
上面我們也說了,ThreadLocalMap是ThreadLocal的靜態內部類,而每個線程獨立的變量副本存儲也是在這個Map中,它是一個定制的哈希表,底層維護了一個Entry 類型的數組類型的數組 table,它的內部提供了set、remove、getEntry等方法。
Entry靜態內部類
這個Entry又是ThreadLocalMap的一個靜態內部類,我們看一下它的源碼:
static class Entry extends WeakReference<ThreadLocal<?>> {/** The value associated with this ThreadLocal. */Object value;Entry(ThreadLocal<?> k, Object v) {super(k);value = v;}
}
Entry 繼承了弱引用 WeakReference<ThreadLocal<?>>,它的 value 字段用于存儲與特定 ThreadLocal 對象關聯的值,key 為弱引用,意味著當 ThreadLocal 外部強引用被置為 null(ThreadLocalInstance=null)時,根據可達性分析,ThreadLocal 實例此時沒有任何一條鏈路引用它,所以系統 GC 的時候 ThreadLocal 會被回收。這種操作看似利用垃圾回收器節省了內存空間,實則存在一個風險,也就是我們下面要說的內存泄露問題!
只具有弱引用的對象,擁有更為短暫的生命周期,在GC線程掃描到它所在的內存區域的時候,一旦發現了只有弱引用的對象的時候,不管內存夠不夠用都會將其回收掉
四、ThreadLocal內存泄漏問題
4.1 內存泄漏的原因
如果非要問ThreadLocal有什么缺點的話,那就是使用不當的時候,會帶來內存泄漏問題。
內存泄漏 是指程序中已動態分配的堆內存由于某種原因程序未釋放或無法釋放,造成系統內存的浪費,導致程序運行速度減慢甚至系統崩潰等嚴重后果。
根據3.1中的分析,我們知道ThreadLocalMap中的使用的key是ThreadLocal的弱引用,Value為強引用,如果ThreadLocal沒有被強引用的話,key會被GC掉,而value依舊存在,若我們采用任何措施的前提下,線程一直運行,那這些value值就會一直存在,過多的占用內存,導致內存泄漏!
4.2 如何解決內存泄漏
如何解決內存泄漏呢,只需要記得在使用完 ThreadLocal 中存儲的內容后將它 remove 掉就可以了。
//ThreadLocal提供的清理方法
public void remove() {//1. 獲取當前線程的ThreadLocalMapThreadLocalMap m = getMap(Thread.currentThread());if (m != null)//2. 從map中刪除以當前ThreadLocal實例為key的鍵值對m.remove(this);
}
/*** ThreadLocalMap中的remove方法*/
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) {//將entry的key置為nulle.clear();//將該entry的value也置為nullexpungeStaleEntry(i);return;}}
}
除此之外,我們還可以使用Java 8引入的InheritableThreadLocal來替代ThreadLocal,它可以在子線程中自動繼承父線程的線程局部變量值,從而避免在創建新線程時重復設置值的問題。但是同樣需要注意及時清理資源以避免內存泄漏。
五、線程間局部變量傳值問題
上面我們提到的Java8中引入的InheritableThreadLocal類,這是實現父子線程間局部變量傳值的關鍵!
InheritableThreadLocal存在于java.lang包中是ThreadLocal的擴展,它有一個特性,那就是當創建一個新的線程時,如果父線程中有一個 InheritableThreadLocal 變量,那么子線程將會繼承這個變量的值。這意味著子線程可以訪問其父線程為此類變量設置的值。我們寫一個小demo感受一下!
public class TestService{// 創建一個 InheritableThreadLocal 變量private static final InheritableThreadLocal<String> inheritableThreadLocal = new InheritableThreadLocal<>();public static void main(String[] args) {// 在主線程中設置值inheritableThreadLocal.set("這是父線程的值");System.out.println("父線程中的值: " + inheritableThreadLocal.get());// 創建一個子線程Thread childThread = new Thread(() -> {// 在子線程中嘗試獲取值,由于使用了 InheritableThreadLocal,這里會獲取到父線程中設置的值System.out.println("子線程中的值: " + inheritableThreadLocal.get());});// 啟動子線程childThread.start();// 等待子線程執行完成try {childThread.join();} catch (InterruptedException e) {e.printStackTrace();}// 主線程結束時清除值,防止潛在的內存泄漏inheritableThreadLocal.remove();}
}
輸出:
父線程中的值: 這是父線程的值
子線程中的值: 這是父線程的值
輸出不出所料,在子線程中獲取的其實是父線程設置的inheritableThreadLocal值。
5.1 父子線程局部變量傳值的實現原理
我們看到上面的輸出后,應該思考這樣的一個問題:子線程是怎么拿到父線程的inheritableThreadLocal值得呢?其實要從子線程的初始化開始說起,在線程Thread的內部,有著這樣的一個初始化方法:
private void init(ThreadGroup g, Runnable target, String name,long stackSize, AccessControlContext acc,// 該參數一般默認是 trueboolean inheritThreadLocals) {// 省略大部分代碼Thread parent = currentThread();// 復制父線程的 inheritableThreadLocals 屬性,實現父子線程局部變量共享if (inheritThreadLocals && parent.inheritableThreadLocals != null) {this.inheritableThreadLocals =ThreadLocal.createInheritedMap(parent.inheritableThreadLocals); }// 省略部分代碼
}
在這里將父線程的inheritableThreadLocals賦值了進來,我們跟入createInheritedMap方法中繼續解析:
// 返回一個ThreadLocalMap,傳值為父線程的
static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {return new ThreadLocalMap(parentMap);
}
//ThreadLoaclMap構建的過程中會調用該構造方法
private ThreadLocalMap(ThreadLocalMap parentMap) {Entry[] parentTable = parentMap.table;int len = parentTable.length;setThreshold(len);table = new Entry[len];// 一個個復制父線程 ThreadLocalMap 中的數據for (int j = 0; j < len; j++) {Entry e = parentTable[j];if (e != null) {@SuppressWarnings("unchecked")ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();if (key != null) {// childValue 方法調用的是 InheritableThreadLocal#childValue(T parentValue)Object value = key.childValue(e.value);Entry c = new Entry(key, value);int h = key.threadLocalHashCode & (len - 1);while (table[h] != null)h = nextIndex(h, len);table[h] = c;size++;}}}
}
在這個構造方法中,我們終于看到了InheritableThreadLocal的身影,childValue()方法就是其中的一個方法,用來給子線程賦父線程的inheritableThreadLocals值;其實InheritableThreadLocal的源碼非常非常的簡單,大部分的實現都取自父類ThreadLocal。
public class InheritableThreadLocal<T> extends ThreadLocal<T> {protected T childValue(T parentValue) {return parentValue;}ThreadLocalMap getMap(Thread t) {return t.inheritableThreadLocals;}void createMap(Thread t, T firstValue) {t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);}
}
六、總結
OK,基于學妹在字節面試的考點,我們又梳理了一遍ThreadLocal,這個類大家還是要好好學一學的,畢竟在日后的工作中,我們肯定會使用到,譬如用它來保存用戶登錄信息,這樣在同一個線程中的任何地方都可以獲取到登錄信息;用于保存事務上下文,這樣在同一個線程中的任何地方都可以獲取到事務上下文等等。
七、結尾彩蛋
如果本篇博客對您有一定的幫助,大家記得留言+點贊+收藏呀。原創不易,轉載請聯系Build哥!
如果您想與Build哥的關系更近一步,還可以關注“JavaBuild888”,在這里除了看到《Java成長計劃》系列博文,還有提升工作效率的小筆記、讀書心得、大廠面經、人生感悟等等,歡迎您的加入!