線程局部變量的實(shí)現(xiàn)?ThreadLocal使用及場(chǎng)景介紹
前言
回老家,實(shí)在太無(wú)聊,于是乎給自己整了一套臺(tái)式機(jī)配置,總價(jià) 1W+,本以為機(jī)器到位后可以打打游戲,學(xué)學(xué)技術(shù)打發(fā)無(wú)聊的時(shí)光。但是我早已不是從前那個(gè)少年了,打 Dota 已經(jīng)找不到大學(xué)時(shí)巔峰的自己,當(dāng)年我一手 SF 真的是打遍天下無(wú)敵手......,和朋友打 LOL 又沒(méi)有精力去學(xué)一個(gè)新的游戲,賊坑。。。
學(xué)技術(shù)又不想學(xué),太懶了?。?!于是乎,寫文章吧...正好年后找工作用得上!今天我們來(lái)談一談 Java 中存儲(chǔ)線程局部變量的類 ThreadLocal
。
ThreadLocal 介紹
This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its {@code get} or {@code set} method) has its own, independently initialized copy of the variable. {@code ThreadLocal} instances are typically private static fields in classes that wish to associate state with a thread (e.g., a user ID or Transaction ID)
上面這段是該類的注釋:該提供線程局部變量。這些變量不同于它們正常的對(duì)應(yīng)變量,因?yàn)槊總€(gè)通過(guò) get()、set()
訪問(wèn) ThreadLocal
變量的線程都有自己的、獨(dú)立初始化的變量副本。ThreadLocal
實(shí)例通常是類中的私有靜態(tài)字段,希望將狀態(tài)與線程關(guān)聯(lián)(例如,用戶ID或事務(wù)ID)。
簡(jiǎn)單來(lái)說(shuō)它的作用是作為一個(gè)數(shù)據(jù)結(jié)構(gòu),可以為每個(gè)線程分別存儲(chǔ)他們私有的數(shù)據(jù)。我們可以暫時(shí)簡(jiǎn)單理解為下面這張圖(實(shí)際上這個(gè)圖是錯(cuò)的)
后面我們會(huì)詳細(xì)介紹它的設(shè)計(jì)原理。
常用 API
方法 | 作用 |
---|---|
public ThreadLocal() | 實(shí)例化對(duì)象 |
ThreadLocal.withInitial(Supplier<? extends S> supplier ) | 實(shí)例化對(duì)象并賦予它每個(gè)線程初始值 |
public void set(T value) | 設(shè)置當(dāng)前線程綁定的變量 |
public T get() | 獲取當(dāng)前線程綁定的變量 |
public void remove() | 移除當(dāng)前線程綁定的變量 |
ThreadLocal 使用場(chǎng)景
Spring 事務(wù)管理器
在 Spring 事務(wù)實(shí)現(xiàn)中,TransactionSynchronizationManager
類中聲明了多個(gè) ThreadLocal
類型的成員變量用以將事務(wù)執(zhí)行過(guò)程中各種上下文信息綁定到當(dāng)前線程,包括當(dāng)前事務(wù)連接對(duì)象、是否可讀、事務(wù)名稱、隔離級(jí)別等
public abstract class TransactionSynchronizationManager { private static final ThreadLocal<Map<Object, Object>> resources = new NamedThreadLocal<>("Transactional resources"); private static final ThreadLocal<Set<TransactionSynchronization>> synchronizations = new NamedThreadLocal<>("Transaction synchronizations"); private static final ThreadLocal<String> currentTransactionName = new NamedThreadLocal<>("Current transaction name"); private static final ThreadLocal<Boolean> currentTransactionReadOnly = new NamedThreadLocal<>("Current transaction read-only status"); private static final ThreadLocal<Integer> currentTransactionIsolationLevel = new NamedThreadLocal<>("Current transaction isolation level"); private static final ThreadLocal<Boolean> actualTransactionActive = new NamedThreadLocal<>("Actual transaction active"); //...... }
SpringMVC 存儲(chǔ)上下文 Request 數(shù)據(jù)
RequestContextHolder
這個(gè)類是 SpringMVC
中提供的持有上下文 Request
的一個(gè)類,內(nèi)部實(shí)現(xiàn)就是有兩個(gè) ThreadLocal
屬性去存儲(chǔ)請(qǐng)求對(duì)象數(shù)據(jù)。
private static final ThreadLocal<RequestAttributes> requestAttributesHolder = new NamedThreadLocal<>("Request attributes"); private static final ThreadLocal<RequestAttributes> inheritableRequestAttributesHolder = new NamedInheritableThreadLocal<>("Request context");
以便于我們?cè)跇I(yè)務(wù)代碼中在沒(méi)有 HttpServletRequest
對(duì)象的位置也可以通過(guò) ThreadLocal
獲取請(qǐng)求頭等信息,比如我前面一篇關(guān)于 OpenFeign
向下游傳遞 header
的文章就用到了它。
PageHelper 分頁(yè)的實(shí)現(xiàn)
之前流行的分頁(yè)插件之一 PageHelper
其分頁(yè)原理也是通過(guò) ThreadLocal
實(shí)現(xiàn),我們使用它進(jìn)行分頁(yè)時(shí)只需要在代碼中調(diào)用靜態(tài)方法
PageHelper.startPage(pageNum,pageSize);
接下來(lái)的第一條 SQL 就會(huì)自動(dòng)進(jìn)行分頁(yè),其實(shí)原理就是它將分頁(yè)參數(shù)封裝到一個(gè) Page
對(duì)象中,然后將 Page
放進(jìn) ThreadLocal
中以達(dá)到 web 環(huán)境中多個(gè)線程互相分頁(yè)不影響,后面就是都雷同的 SQL 拼接了。
存儲(chǔ)用戶身份信息
在很久之前我們用戶登錄信息的存儲(chǔ)通常都是在 Session
中,后來(lái)大多是逐漸用 ThreadLocal
去代替從 Session
獲取用戶登錄信息了。首先我們?cè)谟脩裘看握?qǐng)求需要授權(quán)的接口時(shí),會(huì)讓用戶攜帶請(qǐng)求頭 token
,后端在攔截器中拿到這個(gè) token
去 redis
查詢用戶信息,或者如果這個(gè) token
是 jwt
的話,直接解析它得到用戶信息然后放進(jìn) ThreadLocal
。
@Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { String header = request.getHeader("x-auth-token"); //如果你的實(shí)現(xiàn)是 token 唯一字符串,從 Redis 拿用戶信息 User user = redisTemplate.opsForValue().get(header); //如果你的實(shí)現(xiàn)是 token 是jwt,那直接解析 jwt 拿到用戶信息 //....... if (user != null) { CurrentUser.set(user); return true; } return false; } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { CurrentUser.clear();//請(qǐng)求結(jié)束之后不要忘記清除 }
CurrentUser
類
public class CurrentUser { public static final ThreadLocal<User> USER_THREAD_LOCAL = new ThreadLocal<>(); public static void set(User user){ USER_THREAD_LOCAL.set(user); } public static User get(){ return USER_THREAD_LOCAL.get(); } public static void clear(){ USER_THREAD_LOCAL.remove(); } }
這樣我們?cè)谌魏蔚胤街灰褂?CurrentUser.get()
就能輕松獲取到當(dāng)前登錄用戶。
以上就是幾個(gè) ThreadLocal
常見(jiàn)的場(chǎng)景,其核心理念就是利用 ThreadLocal
的線程隔離特性。
ThreadLocal 和 synchronized
值得注意的是 ThreadLocal
在解決線程安全問(wèn)題上提供了一種不同于傳統(tǒng)并發(fā)安全的解決思路,傳統(tǒng)的 synchronized
或者 Lock
類是出于并發(fā)操作時(shí)讓多個(gè)線程排隊(duì)去訪問(wèn)共享數(shù)據(jù),但是這樣的弊端就是會(huì)造成鎖競(jìng)爭(zhēng),這是以時(shí)間換空間。
而 ThreadLocal
將這個(gè)問(wèn)題換了一個(gè)角度看待,既然并發(fā)安全的問(wèn)題原因是因?yàn)槎鄠€(gè)線程共享一份數(shù)據(jù),那么我現(xiàn)在就讓每個(gè)線程都擁有一份獨(dú)立數(shù)據(jù),它們各自操作自己私有的本地變量,這樣就不會(huì)有并發(fā)安全問(wèn)題,也沒(méi)有鎖競(jìng)爭(zhēng).但是每個(gè)線程都要維護(hù)一份數(shù)據(jù),會(huì)有額外的內(nèi)存開銷,這是以空間換時(shí)間。
實(shí)際項(xiàng)目中我們應(yīng)該用哪種方式,最終還是取決于業(yè)務(wù)場(chǎng)景更適合哪一種。
線程隔離的原理
ThreadLocal.set(T value) 源碼解讀
前面說(shuō)了一些使用場(chǎng)景,這里我們探究一下 ThreadLocal
是如何實(shí)現(xiàn)線程隔離的。這里我們寫個(gè)極致簡(jiǎn)單的例子
ThreadLocal<User> local = ThreadLocal.withInitial(User::new); new Thread(() -> local.set(new User())).start();
這個(gè)例子只有兩行代碼,我們來(lái)看 local.set(T value)
方法的源碼
public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) { map.set(this, value); } else { createMap(t, value); } }
代碼很簡(jiǎn)單,首先拿到當(dāng)前線程,然后根據(jù)當(dāng)前線程拿到一個(gè) Map
數(shù)據(jù)結(jié)構(gòu),將我們傳進(jìn)來(lái)的值設(shè)置到這個(gè) Map
中,那么重點(diǎn)就在這個(gè) getMap()
中,查看其源碼
ThreadLocalMap getMap(Thread t) { return t.threadLocals; }
我們發(fā)現(xiàn)這個(gè)代碼就更簡(jiǎn)單了,直接返回了當(dāng)前線程的一個(gè)成員變量,Thread
類中是這樣定義的
public class Thread implements Runnable { //... ThreadLocal.ThreadLocalMap threadLocals = null; //... }
從這里就可以明白它是如何實(shí)現(xiàn)線程隔離的,我們?cè)O(shè)置的值全都放進(jìn)了當(dāng)前線程對(duì)象的一個(gè)成員變量中存著呢,那當(dāng)然是線程隔離的,你在哪個(gè)線程中去 set()
,那就會(huì)保存到哪個(gè)線程對(duì)象的成員變量中。
接著我們?cè)倏催@行代碼 map.set(this, value);
,很重要,這里的 this
是什么?是當(dāng)前的 ThreadLocal<Integer> local;
對(duì)象,也就是說(shuō)我們 set()
的值實(shí)際上是以當(dāng)前方法的調(diào)用者 local
為 key
,傳入的值為 value
保存起來(lái)的鍵值對(duì)。簡(jiǎn)單的理解為下圖
我們對(duì)于 ThreadLocal
的操作其實(shí)是對(duì) Thread
的成員變量 threadLocals
進(jìn)行操作。那么這個(gè)時(shí)候我們就要改變一下固有的思維,因?yàn)樵谡5乃季S中,我們看到這行代碼 local.set(new User());
腦海中浮現(xiàn)的第一印象都是向 local
的成員變量中進(jìn)行一個(gè)數(shù)據(jù)的賦值,然而在 ThreadLocal
的實(shí)現(xiàn)中,這行代碼的意思是將 ThreadLocal
作為 key
,傳入的值作為 value
存入到當(dāng)前 Thread
對(duì)象的一個(gè)成員變量中。
ThreadLocalMap
上面我們看到了 Thread
類中的成員變量是 ThreadLocalMap
類型的,ThreadLocal、Thread、ThreadLocalMap
三者的類圖關(guān)系為
首先由于我們程序中可能會(huì)聲明多個(gè) ThreadLocal
對(duì)象,那么自然用于存放的數(shù)據(jù)結(jié)構(gòu)就需要類似集合,需要一個(gè)線程可以存儲(chǔ)多個(gè)以 ThreadLocal
為 key
數(shù)據(jù),考慮到查詢的時(shí)間復(fù)雜度以及各方面綜合考慮,Map
結(jié)構(gòu)再適合不過(guò)。
ThreadLocalMap
是 ThreadLocal
的一個(gè)靜態(tài)內(nèi)部類,它的內(nèi)部又聲明了一個(gè)靜態(tài)內(nèi)部類 Entry
來(lái)實(shí)現(xiàn) K/V,這一點(diǎn)類似于 HashMap
。
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
的子類,那么在了解弱引用之后我們會(huì)發(fā)現(xiàn)在特定的場(chǎng)景下,如果不這么設(shè)計(jì)可能造成內(nèi)存泄漏。
弱引用
在分析內(nèi)存泄漏之前我們必須知道 Jvm
中幾種引用類型以及它們的特點(diǎn)。這里不詳細(xì)介紹,只說(shuō)結(jié)論
引用類型 | 回收機(jī)制 |
---|---|
強(qiáng)引用 | 我們程序中聲明的對(duì)象其引用都是強(qiáng)引用,只要其不指向 null ,GC 時(shí)候就不會(huì)被回收,即使內(nèi)存溢出 |
軟引用 | 使用 SoftReference 類構(gòu)造一個(gè)軟引用,與強(qiáng)引用的區(qū)別是當(dāng)內(nèi)存不足,GC 會(huì)回收軟引用指向的對(duì)象 |
弱引用 | 使用 WeakReference 類構(gòu)造一個(gè)弱引用,與軟引用的區(qū)別是,只要觸發(fā) GC 就會(huì)回收弱引用指向的對(duì)象 |
上面的結(jié)論都可以通過(guò)簡(jiǎn)單的代碼來(lái)驗(yàn)證,這里我們主要介紹結(jié)論。
內(nèi)存泄漏
內(nèi)存泄漏與內(nèi)存溢出
- 內(nèi)存溢出 —— 程序中真的內(nèi)存不夠用了。
- 內(nèi)存泄漏 —— 由于代碼問(wèn)題導(dǎo)致程序中本該被釋放的內(nèi)存沒(méi)有被釋放,最終造成 “內(nèi)存不夠用” 的假象。
內(nèi)存圖
這里我們通過(guò)上面的兩行樣例代碼。
ThreadLocal<User> local = ThreadLocal.withInitial(User::new); new Thread(() -> local.set(new User())).start();
結(jié)合分析 ThreadLocalMap、Thread、ThreadLocal
的源碼可以得到一張完整的內(nèi)存圖。
值得注意的是我們是沒(méi)有辦法直接聲明弱引用的,必須通過(guò) WeakReference
去包裹一個(gè)對(duì)象持有弱引用,以下面代碼為例
WeakReference<User> wr = new WeakReference<>(new User());
它在內(nèi)存中是這樣的
所以完整的內(nèi)存圖應(yīng)該能夠理解。
為什么需要弱引用
使用反證法,假設(shè)我們的 Entry
不用弱引用,那么會(huì)出現(xiàn)這樣的情況,我們聲明出來(lái)的 ThreadLocal
對(duì)象,如果我們不想用它了或者說(shuō)在程序中它的生命周期結(jié)束了(實(shí)際上這種場(chǎng)景很少,一般來(lái)說(shuō)我們的 ThreadLocal
對(duì)象都是以 static final
的形式定義在全局,這里只是存在這個(gè)可能),想讓 GC 回收掉它占用的內(nèi)存,那么我們只需要讓沒(méi)有引用指向它即可 ,也就是將 1號(hào)線 干掉。
但是由于 ThreadLocalMap
里面也有持有我們聲明的 ThreadLocal
對(duì)象的強(qiáng)引用,如果我們想要回收的話就必須把這里的強(qiáng)引用也干掉,最好的方法是使用 remove()
方法移除。否則就需要干掉線程里面的 ThreadLocal.ThreadLocalMap threadLocals = null;
這個(gè)屬性,想要干掉這個(gè)屬性就得等線程銷毀,然而實(shí)際業(yè)務(wù)中有的線程是 24h
不間斷執(zhí)行的,也有的線程是位于線程池要被復(fù)用的,所以只要有一個(gè)線程不銷毀,這個(gè) ThreadLocal
對(duì)象就不會(huì)被回收,這就會(huì)產(chǎn)生內(nèi)存泄漏。
但是如果這里 Entry
的 key
是弱引用,只要我們將 1號(hào)線
干掉,下次 GC 的時(shí)候發(fā)現(xiàn)這個(gè) ThreadLocal
對(duì)象只有一個(gè) 2號(hào)線
弱引用指向它,就會(huì)將它回收掉。
public static void function1() { ThreadLocal<User> local = ThreadLocal.withInitial(User::new); local.set(new User()); local.get(); new Thread(() -> { local.set(new User()); User user = local.get(); while (true) { Thread.sleep(1000); System.out.println("測(cè)試"); } }).start(); }
上面這段代碼如果 Entry
是強(qiáng)引用,當(dāng) function1()
結(jié)束之后 local
指向的內(nèi)存不會(huì)被回收,如果是弱引用,就會(huì)被回收。
remove() 防止內(nèi)存泄漏
ThreadLocal
為了防止內(nèi)存泄漏,已經(jīng)用弱引用幫我們解決了一大隱患,難道使用弱引用就能完全避免內(nèi)存泄漏嗎?并不是,還有一種情況,接著上面的章節(jié),當(dāng)我們 GC 將弱引用指向的 ThreadLocal
內(nèi)存回收之后, ThreadLocalMap
里面的 Entry
的 key
就變成 null
了,這樣我們就無(wú)法訪問(wèn)到它原先對(duì)應(yīng)的 value
,所以這個(gè) value
將不會(huì)被回收,這才是實(shí)際場(chǎng)景中真正的內(nèi)存泄漏問(wèn)題。
所以我們?cè)谟猛曛笠欢ㄐ枰謩?dòng)的調(diào)用 remove()
清除當(dāng)前線程的局部變量值,也就是將對(duì)應(yīng)的 Entry(K/V)
刪掉,這樣即使后來(lái) ThreadLocal
對(duì)象被回收,也不會(huì)造成內(nèi)存泄漏問(wèn)題。
值得注意的是我們觀察 set()、get()
源碼會(huì)發(fā)現(xiàn)它其實(shí)都調(diào)用了一個(gè)方法
private int expungeStaleEntry(int staleSlot) { //...... if (k == null) { e.value = null; tab[i] = null; size--; } //...... }
在每次操作的時(shí)候都會(huì)判斷是否存在 key
為 null
的鍵值對(duì),如果存在就會(huì)刪掉,以此來(lái)盡量的避免內(nèi)存泄漏的問(wèn)題,那這是不是意味著即使我們不手動(dòng) remove()
也可以呢?其實(shí)不然,因?yàn)閷?shí)際業(yè)務(wù)中可能會(huì)出現(xiàn)長(zhǎng)時(shí)間不調(diào)用 set()、get()
方法的情況,所以當(dāng)后面的流程里不再需要使用這個(gè)值得時(shí)候,手動(dòng) remove()
是一個(gè)好習(xí)慣,也是阿里巴巴規(guī)范里面的一個(gè)強(qiáng)制規(guī)定。
remove() 防止數(shù)據(jù)錯(cuò)亂
實(shí)際 Web 項(xiàng)目中我們很多場(chǎng)景都會(huì)用到線程池,當(dāng)用完之后將線程對(duì)象歸還到線程池,如果沒(méi)有 remove()
,下個(gè)請(qǐng)求到來(lái),這個(gè)線程被復(fù)用時(shí)發(fā)現(xiàn)這個(gè)數(shù)據(jù)已經(jīng)存在了,就直接拿過(guò)來(lái)用了,這個(gè)問(wèn)題是很嚴(yán)重的,因?yàn)橄喈?dāng)于一個(gè)線程用了另一個(gè)線程的數(shù)據(jù),這會(huì)造成嚴(yán)重的業(yè)務(wù) bug 。
以上就是線程局部變量的實(shí)現(xiàn) ThreadLocal使用及場(chǎng)景介紹的詳細(xì)內(nèi)容,更多關(guān)于線程局部變量ThreadLocal的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
spring boot參數(shù)驗(yàn)證注解@NotNull、@NotBlank和@NotEmpty區(qū)別解析
使用spring boot參數(shù)驗(yàn)證是常常會(huì)使用@NotNull、@NotBlank和@NotEmpty三個(gè)判斷是否不為空的注解,中文都有不能為空的意思,大部分使用者都傻傻分清它們之間到底有什么區(qū)別,今天就讓咱們來(lái)一起探索它們之間的不同吧,感興趣的朋友一起看看吧2024-05-05實(shí)例解析Java關(guān)于static的作用
只要是有學(xué)過(guò)Java的都一定知道static,也一定能多多少少說(shuō)出一些作用和注意事項(xiàng)。文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-04-04教你利用JAVA實(shí)現(xiàn)可以自行關(guān)閉服務(wù)器的方法
今天給大家?guī)?lái)的是關(guān)于Java的相關(guān)知識(shí),文章圍繞著利用JAVA實(shí)現(xiàn)可以自行關(guān)閉服務(wù)器的方法展開,文中有非常詳細(xì)的介紹及代碼示例,需要的朋友可以參考下2021-06-06Java實(shí)現(xiàn)對(duì)視頻進(jìn)行截圖的方法【附ffmpeg下載】
這篇文章主要介紹了Java實(shí)現(xiàn)對(duì)視頻進(jìn)行截圖的方法,結(jié)合實(shí)例形式分析了Java使用ffmpeg針對(duì)視頻進(jìn)行截圖的相關(guān)操作技巧,并附帶ffmpeg.exe文件供讀者下載使用,需要的朋友可以參考下2018-01-01Java自動(dòng)取款機(jī)ATM案例實(shí)現(xiàn)
本文主要介紹了Java自動(dòng)取款機(jī)ATM案例實(shí)現(xiàn),整個(gè)過(guò)程可以分為三部分:登錄賬戶和執(zhí)行取款操作,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2023-08-08springboot學(xué)習(xí)之Thymeleaf模板引擎及原理介紹
本文主要介紹一下SpringBoot給我們推薦的Thymeleaf模板引擎,這模板引擎呢,是一個(gè)高級(jí)語(yǔ)言的模板引擎,他的這個(gè)語(yǔ)法更簡(jiǎn)單而且功能更強(qiáng)大,對(duì)springboot?Thymeleaf模板引擎相關(guān)知識(shí)感興趣的朋友一起看看吧2022-02-02關(guān)于SpringBoot配置文件加載位置的優(yōu)先級(jí)
這篇文章主要介紹了關(guān)于SpringBoot配置文件加載位置的優(yōu)先級(jí),我們也可以通過(guò)spring.config.location來(lái)改變默認(rèn)的配置文件位置,項(xiàng)目打包好后,我們可以通過(guò)命令行的方式在啟動(dòng)時(shí)指定配置文件的位置,需要的朋友可以參考下2023-10-10