關于ThreadLocal,可能很多同學在學習Java的并發編程部分時,都有所耳聞,但是如果要仔細問ThreadLocal是個啥,我們可能也說不清楚,所以這篇博客旨在幫助大家了解ThreadLocal到底是個啥?
1.ThreadLocal是什么?
首先,我們要知道的是,ThreadLocal類位于Java標準庫的java.lang
包中,它是Java中的一個類,我們可以用它來聲明一個ThreadLocal變量,如下:
ThreadLocal<String> local = new ThreadLocal<>();
好的,接下來解釋一下ThreadLocal的含義:
它從名字上看,叫做本地線程變量,意思是說,ThreadLocal中填充的的是當前線程的變量,該變量對其他線程而言是封閉且隔離的,ThreadLocal為變量在每個線程中創建了一個副本,這樣每個線程都可以訪問自己內部的副本變量。
相信上面的文字描述大家會不太理解,簡單來說,就是用ThreadLocal創建的變量,我們可能會在不同的線程中用到,那么為了避免線程安全問題,每個線程都會為自己單獨存一份這個變量,并且單獨使用和修改這個變量,這樣不同的線程之間就各自使用各自的ThreadLocal變量,互不影響。
但是到底具體每個線程是怎樣存儲的這個變量,以及這個變量如何被這個線程調用,這些的底層是如何實現的,請接著向下看。
2.舉例
大家先看一個例子:
public class ThreadLocalTest02 {public static void main(String[] args) {
//創建一個ThreadLocal變量,名為localThreadLocal<String> local = new ThreadLocal<>();
//創建10個線程,并且每次都在不同的線程中加入這個local變量IntStream.range(0, 10).forEach(i -> new Thread(() -> {
//使用set方法設置加入的local內容local.set(Thread.currentThread().getName() + ":" + i);
//然后輸入當前線程存儲的local變量的信息System.out.println("線程:" + Thread.currentThread().getName() + ",local:" + local.get());}).start());}
}
結果如下:
輸出結果:
線程:Thread-0,local:Thread-0:0
線程:Thread-1,local:Thread-1:1
線程:Thread-2,local:Thread-2:2
線程:Thread-3,local:Thread-3:3
線程:Thread-4,local:Thread-4:4
線程:Thread-5,local:Thread-5:5
線程:Thread-6,local:Thread-6:6
線程:Thread-7,local:Thread-7:7
線程:Thread-8,local:Thread-8:8
線程:Thread-9,local:Thread-9:9
上面的結果說明了什么呢?我們在每個線程中都添加了local對象,并且內容是不同的,然后我們再使用get方法輸出local的值。我們發現,我們只使用了一個local對象,但是在十個線程中的值都是不同的,而且它們的值不會相互影響,這就是ThreadLocal的簡單應用。不同的線程對這個local對象有著自己的備份。
3.Set方法
請大家先仔細閱讀一下下面這段源碼,邏輯一點也不難,我加了注釋:
public class ThreadLocal<T> {public void set(T value) {
//先獲取當前線程,例如在線程1中調用了local.set方法,那么這個t就是線程1Thread t = Thread.currentThread();//然后獲取當前線程1中的ThreadLocalMapThreadLocalMap map = getMap(t);//如果map為空,說明此線程還沒有存入任何一個ThreadLocal對象,我們就創建一個ThreadLocalMap
//如果map不為空,那么我們就直接將value存入這個ThreadLocalMap中if (map != null)map.set(this, value);elsecreateMap(t, value);}
大家現在可能會有疑惑,什么是ThreadLocalMap啊?為啥是從當前線程中獲取啊?還有createMap方法,到底是干啥的捏?我們一一進行解釋:
什么是ThreadLocalMap?
我們剛才上面說,我們每個線程都會存儲ThreadLocal對象的備份,那么存儲在哪里呢,答案就是在ThreadLocalMap中,ThreadLocalMap為?ThreadLocal的一個靜態內部類,里面定義了Entry來保存數據,那么既然是map,就會有鍵值對的結構,鍵的位置存的就是我們的ThreadLocal對象,而值存儲的就是通過set方法存入的那個值,例如這一句代碼:local.set( i);那么存到這個線程中的ThreadLocalMap的一個entry中,鍵和值就分別是 local:i
接下來我們就看一下ThreadLocalMap(它是ThreadLocal的一個內部類),還有Entry的結構:
// 內部類ThreadLocalMapstatic class ThreadLocalMap {static class Entry extends WeakReference<ThreadLocal<?>> {Object value;// 內部類Entry,實際存儲數據的地方// Entry的key是ThreadLocal對象,不是當前線程ID或者名稱Entry(ThreadLocal<?> k, Object v) {super(k);value = v;}}// 注意這里維護的是Entry數組private Entry[] table;}
可以看出實際上存儲數據的是Entry,而TheadLocalMap則是一個Entry數組;
ok,了解了這個結構后,我們又回到宏觀的角度看待一下問題,剛才說每個線程都有自己的備份,并且這些備份是當前線程獨有的,那么既然上面說存儲數據的是ThreadLocalMap,并且每個線程都有自己的獨有的一份,那么這個ThreadLocalMap到底存在哪里呢?答案就是:ThreadLocalMap是作為Thread類的一個私有屬性實現的,這樣就可以保證每個Thread線程都有自己獨一份的TheadLocalMap來存儲自己的Threadlocal變量。
public class Thread {/* ... 省略其他代碼 ... *//*** ThreadLocalMap實例,用于存儲ThreadLocal變量的鍵值對*/ThreadLocal.ThreadLocalMap threadLocals = null;/* ... 省略其他代碼 ... */
}
?好好好,現在我們算是知道了,原來為每個線程存儲這些ThreadLocal變量的,就是Thread類中的屬性threadLocals?
那么這樣我們就能解釋為什么要從線程中獲取map了,看一下剛才的set方法中的這一句,我們就知道為啥要從線程中獲取了:
//然后獲取當前線程1中的ThreadLocalMapThreadLocalMap map = getMap(t);
接著就是后面的代碼,相信大家也就能明白為什么要這樣寫啦:
//如果map為空,說明此線程還沒有存入任何一個ThreadLocal對象,我們就創建一個ThreadLocalMap
//如果map不為空,那么我們就直接將value存入這個ThreadLocalMap中if (map != null)map.set(this, value);elsecreateMap(t, value);}
這是createMap方法,看到它給什么賦值嗎,就是我們剛才說的Thread線程類中的那個存儲ThreadLocalMap的屬性哦~
void createMap(Thread t, T firstValue) {t.threadLocals = new ThreadLocalMap(this, firstValue);}
ok,set方法就介紹到這里了
4.get方法
get方法的作用很簡單,它通過local對象調用,返回當前線程的以local對象為鍵,對應的那個值即可;例如 System.out.println(local.get());就是輸出當前線程的local對象當時通過set方法存入的值。
get方法的源碼如下:
public T get() {
//先獲取當前調用get方法的線程Thread t = Thread.currentThread();//然后獲取此線程的ThreadLocalMap對象,這里面存儲著local鍵值對ThreadLocalMap map = getMap(t);/*如果map不為空,就在map里面尋找鍵為this的entry,為什么是this呢,因為當前類
是ThreadLocal類,而get方法通過local.get()的方式調用,所以這里的this就指的
是這個local對象,也就是entry的鍵。如果找的了這個以local為鍵的entry,我們就
返回對應的值即可。
*/if (map != null) {ThreadLocalMap.Entry e = map.getEntry(this);if (e != null) {@SuppressWarnings("unchecked")T result = (T)e.value;return result;}}return setInitialValue();}
ok,ThreadLocal的具體應用和get方法就介紹到這里
5.ThreadLocal的結構
有了上面的基礎,我們現在來看一下他在內存中的結構:
6.內存泄漏問題
仔細看下ThreadLocal內存結構就會發現,Entry數組對象通過ThreadLocalMap最終被Thread持有,并且是強引用。也就是說Entry數組對象的生命周期和當前線程一樣。即使ThreadLocal對象被回收了,Entry數組對象也不一定被回收,這樣就有可能發生內存泄漏。ThreadLocal在設計的時候就提供了一些補救措施:
- Entry的key是弱引用的ThreadLocal對象,很容易被回收,導致key為null(但是value不為null)。所以在調用get()、set(T)、remove()等方法的時候,會自動清理key為null的Entity。
- remove()方法就是用來清理無用對象,防止內存泄漏的。所以每次用完ThreadLocal后需要手動remove()。
解決辦法:使用完ThreadLocal
后,執行remove
操作,避免出現內存溢出情況。
如同?lock
?的操作最后要執行解鎖操作一樣,ThreadLocal
使用完畢一定記得執行remove 方法,清除當前線程的數值。如果不remove
?當前線程對應的VALUE
?,就會一直存在這個值。
這里復習一下對象的強引用、軟引用、弱引用
1.強引用
我們平日里面的用到的new了一個對象就是強引用,例如
Object obj = new Object();
當JVM的內存空間不足時,寧愿拋出OutOfMemoryError使得程序異常終止也不愿意回收具有強引用的存活著的對象!2.軟引用
當JVM認為內存空間不足時,就回去試圖回收軟引用指向的對象,也就是說在JVM拋出
OutOfMemoryError
之前,會去清理軟引用對象。3.弱引用
在GC的時候,不管內存空間足不足都會回收這個對象,同樣也可以配合
ReferenceQueue
使用,也同樣適用于內存敏感的緩存。ThreadLocal
中的key就用到了弱引用。
7.最后我們還要知道為什么要使用ThreadLocal?
ThreadLocal類在多線程編程中有幾個重要的用途和優勢:
-
線程隔離:ThreadLocal提供了一種將數據與線程關聯的機制。通過使用ThreadLocal,可以為每個線程創建獨立的變量副本,使得每個線程都可以獨立地訪問和修改自己的變量副本,而不會干擾其他線程的數據。這樣可以實現線程間的數據隔離,避免了線程安全問題。
-
狀態傳遞:ThreadLocal可以用于在同一個線程的多個方法之間傳遞狀態信息,而無需在方法參數中顯式傳遞。通過將狀態信息存儲在ThreadLocal變量中,不同的方法可以通過ThreadLocal訪問和修改共享的狀態,而無需顯式傳遞參數。這樣可以簡化方法的調用,提高代碼的可讀性和可維護性。
-
線程上下文管理:有些情況下,需要在整個線程執行期間共享某些上下文信息,比如用戶認證信息、數據庫連接等。通過將這些信息存儲在ThreadLocal中,可以在同一線程的任何地方方便地訪問和使用這些信息,而無需顯式傳遞或在每個方法中重復獲取。
-
避免鎖競爭:在某些情況下,使用ThreadLocal可以避免使用鎖來同步對共享變量的訪問。由于每個線程都有自己的變量副本,線程之間不會產生競爭條件,從而避免了鎖競爭和同步開銷,提高了程序的性能。