ThreadLocal原理深入解析
1. 從一次專案經歷說起
在上家公司做spark的任務排程系統時,碰到過這麼一個需求:
1.任務由一個執行緒執行,同時在執行過程中會建立多個執行緒執行子任務,子執行緒在執行子任務時又會建立子執行緒執行子任務的子任務。整個任務結構就像一棵高度為3的樹。
2.每個任務在執行過程中會生成一個任務ID,我需要把這個任務ID傳給子執行緒執行的子任務,子任務同時也會生成自己的任務ID,並把自己的任務ID向自己的子任務傳遞。
流程可由下圖所示
解決方案有很多,比如藉助外部儲存如資料庫,或者自己在記憶體中維護一個儲存ID的資料結構。考慮到系統健壯性和可維護性,最後採用了jdk中的InheritableThreadLocal
來實現這個需求。
來看下InheritableThreadLocal的結構
public class InheritableThreadLocalextends ThreadLocal {
InheritableThreadLocal繼承自ThreadLocal,ThreadLocal可以說是一個儲存執行緒私有變數的容器(當然這個說法嚴格來說不準確,後面我們就知道為什麼),而InheritableThreadLocal正如Inheritable所暗示的那樣,它是可繼承的:使用它可使子執行緒繼承父執行緒的所有執行緒私有變數。因此我寫了個工具類,底層使用InheritableThreadLocal來儲存任務的ID,並且使該ID能夠被子執行緒繼承。
public class InheritableThreadLocalUtils { private static final ThreadLocallocal = new InheritableThreadLocal(); public static void set(Integer t) { local.set(t); } public static Integer get() { return local.get(); } public static void remove() { local.remove(); } }
可以透過這個工具類的set方法和get方法分別實現任務ID的存取。然而在Code Review的時候,有同事覺得我這程式碼寫的有問題:原因大概是InheritableThreadLocal在這裡只有一個,子執行緒的任務ID在儲存的時候會相互覆蓋掉。真的會這樣嗎?為此我們用程式碼測試下:
public static void main(String[] args) { ExecutorService executorService = Executors.newCachedThreadPool(); for(int i=0;i這段程式碼開啟了10個執行緒標號從0到9,我們在每個執行緒中將對應的標號儲存到InheritableThreadLocal,然後開啟一個子執行緒,在子執行緒中獲取InheritableThreadLocal中的變數。最後的結果如下
每個執行緒都準確的獲取到了父執行緒對應的ID,可見並沒有覆蓋的問題。InheritableThreadLocal確實是用來儲存和獲取執行緒私有變數的,但是真實的變數並不是儲存在這個InheritableThreadLocal物件中,它只是為我們存取執行緒私有變數提供了入口而已。因為InheritableThreadLocal只是在ThreadLocal的基礎上提供了繼承功能,為了弄清這個問題我們研究下ThreadLocal的原始碼。
2. ThreadLocal原始碼解析
ThreadLocal主要方法有兩個,一個set用來儲存執行緒私有變數,一個get用來獲取執行緒私有變數。
2.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(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); }Thread t = Thread.currentThread()獲取了當前執行緒例項t,繼續跟進第二行的getMap方法,
/** * Get the map associated with a ThreadLocal. Overridden in * InheritableThreadLocal. * * @param t the current thread * @return the map */ThreadLocalMap getMap(Thread t) { return t.threadLocals; }t是執行緒例項,而threadLocals明顯是t的一個成員變數,進入一探究竟
/* ThreadLocal values pertaining to this thread. This map is maintained * by the ThreadLocal class. */ ThreadLocal.ThreadLocalMap threadLocals = null;ThreadLocalMap是個什麼結構?
static class ThreadLocalMap { /** * 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> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal> k, Object v) { super(k); value = v; } } ThreadLocalMap是類Thread中的一個靜態內部類,看起來像一個HashMap,但和HashMap又有些不一樣(關於它們的區別後面會講),那我們就把它當一個特殊的HashMap好了。因此set方法中第二行程式碼
ThreadLocalMap map = getMap(t)是透過執行緒例項t得到一個ThreadLocalMap。接下來的程式碼if (map != null) map.set(this, value); else 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 */void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue); }如果這個threadlocalmap為null,先建立一個threadlocalmap,然後以當前threadlocal物件為key,以要儲存的變數為值儲存到threadlocalmap中。
2.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(); }首先獲取當前執行緒例項t,然後透過getMap(t)方法得到threadlocalmap(ThreadLocalMap是Thread的成員變數)。若這個map不為null,則以threadlocal為key獲取執行緒私有變數,否則執行setInitialValue方法。看下這個方法的原始碼
private T setInitialValue() { T value = initialValue(); Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); return value; }protected T initialValue() { return null; }首先獲取threadlocal的初始化值,預設為null,可以透過重寫自定義該值;如果threadlocalmap為null,先建立一個;以當前threadlocal物件為key,以初始化值為value存入map中,最後返回這個初始化值。
2.3 ThreadLocal原始碼總結
總的來說,ThreadLocal的原始碼並不複雜,但是邏輯很繞。現總結如下:
1.ThreadLocal物件為每個執行緒存取私有的本地變數提供了入口,變數實際儲存線上程例項的內部一個叫ThreadLocalMap的資料結構中。
2.ThreadLocalMap是一個類HashMap的資料結構,Key為ThreadLoca物件(其實是一個弱引用),Value為要儲存的變數值。
3.使用ThreadLocal進行存取,其實就是以ThreadLocal物件為隱含的key對各個執行緒私有的Map進行存取。
可以用下圖的記憶體影像幫助理解和記憶
3. ThreadLocalMap詳解
先看原始碼
static class ThreadLocalMap { /** * 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> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal> k, Object v) { super(k); value = v; } }
3.1 ThreadLocalMap的key為弱引用
ThreadLocalMap的key並不是ThreadLocal,而是WeakReference
3.2 為何要用弱引用
減少了記憶體洩漏。試想我曾今儲存了一個ThreadLocal物件到ThreadLocalMap中,但後來我不需要這個物件了,只有ThreadLocalMap中的key還引用了該物件。如果這是個強引用的話,該物件將一直無法回收。因為我已經失去了其他所有該物件的外部引用,這個ThreadLocal物件將一直存在,而我卻無法訪問也無法回收它,導致記憶體洩漏。又因為ThreadLocalMap的生命週期和執行緒例項的生命週期一致,只要該執行緒一直不退出,比如執行緒池中的執行緒,那麼這種記憶體洩漏問題將會不斷積累,直到導致系統奔潰。而如果是弱引用的話,當ThreadLocal失去了所有外部強引用的話,下次垃圾收集該ThreadLocal物件將被回收,對應的ThreadLocalMap中的key將為null。下次get和set方法被執行時將會對key為null的Entry進行清理。有效的減少了記憶體洩漏的可能和影響。
3.3 如何真正避免記憶體洩漏
及時呼叫ThreadLocal的remove方法
及時銷燬執行緒例項
4. 總結
ThreadLocal為我們存取執行緒私有變數提供了入口,變數實際儲存線上程例項的map結構中;使用它可以讓每個執行緒擁有一份共享變數的複製,以非同步的方式解決多執行緒對資源的爭用
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/855/viewspace-2804878/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- 併發——深入分析ThreadLocal的實現原理thread
- ThreadLocal 解析thread
- 解析ThreadLocalthread
- ThreadLocal解析thread
- 深入理解ThreadLocalthread
- Java ThreadLocal解析Javathread
- ThreadLocal原理分析thread
- ThreadLocal 原理分析thread
- 深入解析 ResNet:實現與原理
- 深入解析vue響應式原理Vue
- ThreadLocal原始碼解析thread原始碼
- ThreadLocal用法及原理thread
- 深入原始碼解析 tapable 實現原理原始碼
- 深入解析 oracle drop table內部原理Oracle
- 深入解析Vue中的computed工作原理Vue
- Thread、ThreadLocal原始碼解析thread原始碼
- 17_深入解析Oracle undo原理(1)_transactionOracle
- ThreadLocal原理用法詳解ThreadLocal記憶體洩漏thread記憶體
- 深入理解ThreadLocal及其變種thread
- ThreadLocal底層原始碼解析thread原始碼
- JUC---ThreadLocal原理詳解thread
- 【多執行緒】ThreadLocal原理執行緒thread
- ThreadLocal及InheritableThreadLocal的原理剖析thread
- 深入淺出MyBatis:MyBatis解析和執行原理MyBatis
- Java併發程式設計:ThreadLocal的使用以及實現原理解析Java程式設計thread
- ThreadLocal原始碼解析-Java8thread原始碼Java
- Java中ThreadLocal的用法和原理Javathread
- ThreadLocal的正確使用與原理thread
- 原始碼|ThreadLocal的實現原理原始碼thread
- ThreadLocal 原理和使用場景分析thread
- 19_深入解析Oracle undo原理(3)_ktuxe詳解OracleUX
- 20_深入解析Oracle undo原理(4)_ktuxc詳解OracleUX
- Java併發程式設計:深入剖析ThreadLocalJava程式設計thread
- 揭開神秘面紗——深入淺出ThreadLocalthread
- 一次ThreadLocal原始碼解析之旅thread原始碼
- ThreadLocal原理記錄,別被坑了!!thread
- 深入解析 PyTorch 的 BatchNorm2d:原理與實現PyTorchBATORM
- 深入解析 Apache BookKeeper 系列:第二篇 — 寫操作原理Apache