# ThreadLocal
? ThreadLocal意為線程本地變量,用于解決多線程并發時訪問共享變量的問題。
? 所謂的共享變量指的是在堆中的實例、靜態屬性和數組;對于共享數據的訪問受Java的內存模型(JMM)的控制,其模型如下:
:-: 
【參考:《Java并發編程的藝術》P22】
?
? 每個線程都會有屬于自己的本地內存,在堆(也就是上圖的主內存)中的變量在被線程使用的時候會被復制一個副本線程的本地內存中,當線程修改了共享變量之后就會通過JMM管理控制寫會到主內存中。
? 很明顯,在多線程的場景下,當有多個線程對共享變量進行修改的時候,就會出現線程安全問題,即數據不一致問題。常用的解決方法是對訪問共享變量的代碼加鎖(synchronized或者Lock)。但是這種方式對性能的耗費比較大。在JDK1.2中引入了ThreadLocal類,來修飾共享變量,使每個線程都**單獨擁有一份共享變量**,這樣就可以做到線程之間對于共享變量的隔離問題。
? 當然鎖和ThreadLocal使用場景還是有區別的,具體區別如下:
| | synchronized(鎖) | ThreadLocal |
| --- | --- | --- |
| 原理 | 同步機制采用了時間換空間的方式,只提供一份變量,讓不同線程**排隊**訪問(臨界區排隊) | 采用空間換時間的方式,為每一個線程都提**供一份變量的副本,從而實現同時訪問而互不相干擾** |
| 側重點 | 多個線程之間訪問資源的**同步** | 多線程中讓每個線程之間的數據相互**隔離** |
?
## 1\. ThreadLocal的使用及原理
### 1.1 使用
1. 一般都會將ThreadLocal聲明成一個靜態字段,同時初始化如下:
~~~
?static ThreadLocal<Object> threadLocal = new ThreadLocal<>();
~~~
其中Object就是原本堆中共享變量的數據。
例如,有個User對象需要在不同線程之間進行隔離訪問,可以定義ThreadLocal如下:
~~~
?public class Test {
? ? ?static ThreadLocal<User> threadLocal = new ThreadLocal<>();
?}
~~~
?
2. 常用的方法
* set(T value):設置線程本地變量的內容。
* get():獲取線程本地變量的內容。
* remove():移除線程本地變量。注意在線程池的線程復用場景中在線程執行完畢時一定要調用remove,避免在線程被重新放入線程池中時被本地變量的舊狀態仍然被保存。
~~~
?public class Test {
? ? ?static ThreadLocal<User> threadLocal = new ThreadLocal<>();
? ? ?
? ? ?public void m1(User user) {
? ? ? ? ?threadLocal.set(user);
? ? }
? ? ?
? ? ?public void m2() {
? ? ? ? ?User user = threadLocal.get();
? ? ? ? ?// 使用
? ? ? ? ?
? ? ? ? ?// 使用完清除
? ? ? ? ?threadLocal.remove();
? ? }
?}
~~~
?
### 1.2 原理
? 那么如何究竟是如何實現在每個線程里面保存一份單獨的本地變量呢?首先,在Java中的線程是什么呢?是的,就是一個Thread類的實例對象!**而一個實例對象中實例成員字段的內容肯定是這個對象獨有的**,所以我們也可以將保存ThreadLocal線程本地變量作為一個Thread類的**成員字段**,這個成員字段就是:
~~~
?/* ThreadLocal values pertaining to this thread. This map is maintained
? * by the ThreadLocal class. */
?ThreadLocal.ThreadLocalMap threadLocals = null;
~~~
? 是一個在ThreadLocal中定義的Map對象,保存了該線程中的所有本地變量。ThreadLocalMap中的Entry的定義如下:
~~~
?static class Entry extends WeakReference<ThreadLocal<?>> {
? ? ?/** The value associated with this ThreadLocal. */
? ? ?Object value;
? ? ?// key為一個ThreadLocal對象,v就是我們要在線程之間隔離的對象
? ? ?Entry(ThreadLocal<?> k, Object v) {
? ? ? ? ?super(k);
? ? ? ? ?value = v;
? ? }
?}
~~~
`ThreadLocalMap和Entry都在ThreadLocal中定義。`
?
> ThreadLocal::set方法的原理
set方法的源碼如下:
~~~
?public void set(T value) {
? ? ?// 獲取當前線程
? ? ?Thread t = Thread.currentThread();
? ? ?// 獲取當前線程的threadLocals字段
? ? ?ThreadLocalMap map = getMap(t);
? ? ?// 判斷線程的threadLocals是否初始化了
? ? ?if (map != null) {
? ? ? ? ?map.set(this, value);
? ? } else {
? ? ? ? ?// 沒有則創建一個ThreadLocalMap對象進行初始化
? ? ? ? ?createMap(t, value);
? ? }
?}
~~~
createMap方法的源碼如下:
~~~
?void createMap(Thread t, T firstValue) {
? t.threadLocals = new ThreadLocalMap(this, firstValue);
?}
~~~
map.set方法的源碼如下:
~~~
?/**
?* 往map中設置ThreadLocal的關聯關系
?* set中沒有使用像get方法中的快速選擇的方法,因為在set中創建新條目和替換舊條目的內容一樣常見,
?* 在替換的情況下快速路徑通常會失敗(對官方注釋的翻譯)
?*/
?private void set(ThreadLocal<?> key, Object value) {
? ? ?// map中就是使用Entry[]數據保留所有的entry實例
? ? ?Entry[] tab = table;
? ? ?int len = tab.length;
? ? ?// 返回下一個哈希碼,哈希碼的產生過程與神奇的0x61c88647的數字有關
? ? ?int i = key.threadLocalHashCode & (len-1);
??
? ? ?for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
? ? ? ? ?ThreadLocal<?> k = e.get();
? ? ? ? ?if (k == key) {
? ? ? ? ? ? ?// 已經存在則替換舊值
? ? ? ? ? ? ?e.value = value;
? ? ? ? ? ? ?return;
? ? ? ? }
? ? ? ? ?if (k == null) {
? ? ? ? ? ? ?// 在設置期間清理哈希表為空的內容,保持哈希表的性質
? ? ? ? ? ? ?replaceStaleEntry(key, value, i);
? ? ? ? ? ? ?return;
? ? ? ? }
? ? }
? ? ?tab[i] = new Entry(key, value);
? ? ?int sz = ++size;
? ? ?// 擴容邏輯
? ? ?if (!cleanSomeSlots(i, sz) && sz >= threshold)
? ? ? ? ?rehash();
?}
~~~
?
> Thread::get方法的原理
~~~
?public T get() {
? ? ?Thread t = Thread.currentThread();
? ? ?ThreadLocalMap map = getMap(t);
? ? ?if (map != null) {
? ? ? ? ?// 獲取ThreadLocal對應保留在Map中的Entry對象
? ? ? ? ?ThreadLocalMap.Entry e = map.getEntry(this);
? ? ? ? ?if (e != null) {
? ? ? ? ? ? ?@SuppressWarnings("unchecked")
? ? ? ? ? ? ?// 獲取ThreadLocal對象對應的值
? ? ? ? ? ? ?T result = (T)e.value;
? ? ? ? ? ? ?return result;
? ? ? ? }
? ? }
? ? ?// map還沒有初始化時創建map對象,并設置null,同時返回null
? ? ?return setInitialValue();
?}
~~~
?
> ThreadLocal::remove()方法原理
~~~
?public void remove() {
? ? ?ThreadLocalMap m = getMap(Thread.currentThread());
? ? ?// 鍵在直接移除
? ? ?if (m != null) {
? ? ? ? ?m.remove(this);
? ? }
?}
~~~
?
ThreadLocalMap的類結構體系如下:
:-: 
?
### 1.3 ThreadLocal設計
1. 在JDK早期的設計中,每個ThreadLocal都有一個map對象,將線程作為map對象的key,要存儲的變量作為map的value,但是現在已經不是這樣了。
2. JDK8之后,每個Thread維護一個ThreadLocalMap對象,這個Map的key是ThreadLocal實例本身,value是存儲的值要隔離的變量,是泛型,其具體過程如下:
- 每個Thread線程內部都有一個Map(ThreadLocalMap::threadlocals);
- Map里面存儲ThreadLocal對象(key)和線程的變量副本(value);
- Thread內部的Map由ThreadLocal維護,由ThreadLocal負責向map獲取和設置變量值;
- 對于不同的線程,每次獲取副本值時,別的線程不能獲取當前線程的副本值,就形成了數據之間的隔離。
JDK8之后設計的好處在于:
- 每個Map存儲的Entry的數量變少,在實際開發過程中,ThreadLocal的數量往往要少于Thread的數量,Entry的數量減少就可以減少哈希沖突。
- 當Thread銷毀的時候,ThreadLocalMap也會隨之銷毀,減少內存使用,早期的ThreadLocal并不會自動銷毀。
<hr>
使用ThreadLocal的好處
1. 保存每個線程綁定的數據,在需要的地方可以直接獲取,避免直接傳遞參數帶來的代碼耦合問題;
2. 各個線程之間的數據相互隔離卻又具備并發性,避免同步方式帶來的性能損失。
?
## 2. ThreadLocal內存泄露問題
? ? 內存泄露問題:指程序中動態分配的堆內存由于某種原因沒有被釋放或者無法釋放,造成系統內存的浪費,導致程序運行速度減慢或者系統奔潰等嚴重后果。內存泄露堆積將會導致內存溢出。
? ? ThreadLocal的內存泄露問題一般考慮和Entry對象有關,在上面的Entry定義可以看出ThreadLocal::Entry被弱引用所修飾。**JVM會將弱引用修飾的對象在下次垃圾回收中清除掉**。這樣就可以實現ThreadLocal的生命周期和線程的生命周期解綁。但實際上并不是使用了弱引用就A會發生內存泄露問題,考慮下面幾個過程:
1. 使用強引用

? 當ThreadLocal Ref被回收了,由于在Entry使用的是強引用,在Current Thread還存在的情況下就存在著到達Entry的引用鏈,無法清除掉ThreadLocal的內容,同時Entry的value也同樣會被保留;也就是說就算使用了強引用仍然會出現內存泄露問題。
2. 使用弱引用
:-: 
? ? 當ThreadLocal Ref被回收了,由于在Entry使用的是弱引用,因此在下次垃圾回收的時候就會將ThreadLocal對象清除,這個時候Entry中的KEY=null。但是由于ThreadLocalMap中任然存在Current Thread Ref這個強引用,因此Entry中value的值任然無法清除。還是存在內存泄露的問題。
? ? 由此可以發現,使用ThreadLocal造成內存泄露的問題是因為:ThreadLocalMap的生命周期與Thread一致,如果不手動清除掉Entry對象的話就可能會造成內存泄露問題。**因此,需要我們在每次在使用完之后需要手動的remove掉Entry對象。**
> 那么為什么使用弱引用?
? ? 避免內存泄露的兩種方式:使用完ThreadLocal,調用其remove方法刪除對應的Entry 或者使用完ThreadLocal,當前Thread也隨之運行結束。第二種方法在使用線程池技術時是不可以實現的。
? 所以一般都是自己手動調用remove方法,調用remove方法弱引用和強引用都不會產生內存泄露問題,使用弱引用的原因如下:
? 在ThreadLocalMap的set/getEntry中,會對key進行判斷,**如果key為null,那么value也會被設置為null**,這樣即使在忘記調用了remove方法,當ThreadLocal被銷毀時,對應value的內容也會被清空。**多一層保障!**
> 總結:存在內存泄露的有兩個地方:ThreadLocal和Entry中Value;最保險還是要注意要自己及時調用remove方法!!!
## 三、ThreadLocal的應用場景
> 場景一:在重入方法中替代參數的顯式傳遞
假如在我們的業務方法中需要調用其他方法,同時其他方法都需要用到同一個對象時,可以使用ThreadLocal替代參數的傳遞或者static靜態全局變量。這是因為使用參數傳遞造成代碼的耦合度高,使用靜態全局變量在多線程環境下不安全。當該對象用ThreadLocal包裝過后,就可以保證在該線程中獨此一份,同時和其他線程隔離。
例如在Spring的@Transaction事務聲明的注解中就使用ThreadLocal保存了當前的Connection對象,避免在本次調用的不同方法中使用不同的Connection對象。
> 場景二:全局存儲用戶信息
可以嘗試使用ThreadLocal替代Session的使用,當用戶要訪問需要授權的接口的時候,可以現在攔截器中將用戶的Token存入ThreadLocal中;之后在本次訪問中任何需要用戶用戶信息的都可以直接沖ThreadLocal中拿取數據。例如自定義獲取用戶信息的類AuthHolder:
~~~
?public class AuthNHolder {
? ? ?private static final ThreadLocal<Map<String,String>> threadLocal = new ThreadLocal<>();
? ? ?public static void map(Map<String,String> map){
? ? ? ? ?threadLocal.set(map);
? ? }
? ? ?// 獲取用戶id
? ? ?public static String userId(){
? ? ? ? ?return get("userId");
? ? }
? ? ?// 根據鍵值獲取對應的信息
? ? ?public static String get(String key){
? ? ? ? ?Map<String,String> map = getMap();
? ? ? ? ?return map.get(key);
? ? }
? ? ?// 用完清空ThreadLocal
? ? ?public static void clear(){
? ? ? ? ?threadLocal.remove();
? ? }
?}
~~~
備注:參考博文[https://cloud.tencent.com/developer/article/1636025](https://cloud.tencent.com/developer/article/1636025)。ThreadLocal里面封裝的value只是一個例子,根據具體業務需求改就行了。
> 場景三:解決線程安全問題
依賴于ThreadLocal本身的特性,對于需要進行線程隔離的變量可以使用ThreadLocal進行封裝。
## 四、總結
1. ThreadLocal更像是對其他類型變量的一層包裝,通過ThreadLocal的包裝使得該變量可以在`線程之間隔離`和`當前線程全局共享`。
2. 線程的隔離性和變量的線程全局共享性得益于在每個Thread類中的threadlocals字段。(從類實例對象的角度抽象的去看Java中的線程!!!)
3. ThreadLocalMap中Entry的Key不管是否使用弱引用都有內存泄露的可能。引起內存泄露主要在于ThreadLocal對象和Entry中的Value對象,因此要確保每次使用完之后都remove掉Entry!
【參考內容】
1. <a href="https://www.liaoxuefeng.com/wiki/1252599548343744/1306581251653666"> 廖雪峰的官方網站-使用TreadLocal</a>
2. 弱引用相關知識:《深入理解Java虛擬機》垃圾回收部分。
- 第一章 Java基礎
- ThreadLocal
- Java異常體系
- Java集合框架
- List接口及其實現類
- Queue接口及其實現類
- Set接口及其實現類
- Map接口及其實現類
- JDK1.8新特性
- Lambda表達式
- 常用函數式接口
- stream流
- 面試
- 第二章 Java虛擬機
- 第一節、運行時數據區
- 第二節、垃圾回收
- 第三節、類加載機制
- 第四節、類文件與字節碼指令
- 第五節、語法糖
- 第六節、運行期優化
- 面試常見問題
- 第三章 并發編程
- 第一節、Java中的線程
- 第二節、Java中的鎖
- 第三節、線程池
- 第四節、并發工具類
- AQS
- 第四章 網絡編程
- WebSocket協議
- Netty
- Netty入門
- Netty-自定義協議
- 面試題
- IO
- 網絡IO模型
- 第五章 操作系統
- IO
- 文件系統的相關概念
- Java幾種文件讀寫方式性能對比
- Socket
- 內存管理
- 進程、線程、協程
- IO模型的演化過程
- 第六章 計算機網絡
- 第七章 消息隊列
- RabbitMQ
- 第八章 開發框架
- Spring
- Spring事務
- Spring MVC
- Spring Boot
- Mybatis
- Mybatis-Plus
- Shiro
- 第九章 數據庫
- Mysql
- Mysql中的索引
- Mysql中的鎖
- 面試常見問題
- Mysql中的日志
- InnoDB存儲引擎
- 事務
- Redis
- redis的數據類型
- redis數據結構
- Redis主從復制
- 哨兵模式
- 面試題
- Spring Boot整合Lettuce+Redisson實現布隆過濾器
- 集群
- Redis網絡IO模型
- 第十章 設計模式
- 設計模式-七大原則
- 設計模式-單例模式
- 設計模式-備忘錄模式
- 設計模式-原型模式
- 設計模式-責任鏈模式
- 設計模式-過濾模式
- 設計模式-觀察者模式
- 設計模式-工廠方法模式
- 設計模式-抽象工廠模式
- 設計模式-代理模式
- 第十一章 后端開發常用工具、庫
- Docker
- Docker安裝Mysql
- 第十二章 中間件
- ZooKeeper