面試官:不懂ThreadLocal,還談什麼併發程式設計?

前言

什麼是ThreadLocal?

查閱原始碼註釋得知,此類提供執行緒區域性變數。何為執行緒區域性變數?意思就是ThreadLocal儲存的變數屬於當前執行緒,對其他執行緒是隔離的。ThreadLocal為每個執行緒透過它的 get() 或 set() 方法來建立獨立初始化的變數副本;ThreadLocal例項通常是類中希望將狀態與執行緒相關聯的私有靜態欄位(例如,使用者ID 或 事務ID);只要執行緒處於活動狀態且ThreadLocal例項是可訪問的,每個執行緒都持有對其執行緒區域性變數副本的隱式引用;線上程消失後,它的所有執行緒本地例項副本都將受到垃圾回收(除非存在對這些副本的其他引用)。

使用場景

執行緒間的資料隔離

進行事務操作,儲存執行緒事務資訊

資料庫連線、Session會話管理

在進行物件跨層傳遞時,打破層次間的約束

。。。

原始碼部分

ThreadLocal是一個泛型類,透過泛型可以指定要儲存的型別,原始碼中只提供了一個構造方法,這個構造方法通常是單獨使用的,也可以配合 initialValue() 方法使用,重寫該方法是在ThreadLocal例項化時提供一個初始值。實現方式如下面程式碼所示:

構造方法

//方式一:例項化 ThreadLocal 並設定一個初始值ThreadLocal threadId = new ThreadLocal() { @Override protected Integer initialValue() { return 1; }};//方式二:相比方式一ThreadLocal提供了一個靜態方法 withInitial 程式碼看起來更簡潔,更優雅ThreadLocal threadId = ThreadLocal。withInitial(() -> 1);//檢視原始碼:Supplier介面提供了一個get方法並且標記為@FunctionalInterface 支援Lambda表示式寫法public static ThreadLocal withInitial(Supplier<? extends S> supplier) { return new SuppliedThreadLocal<>(supplier);}//檢視原始碼:SuppliedThreadLocal是ThreadLocal中finnal修飾的靜態內部類,繼承了ThreadLocal並且重寫了initialValue()static final class SuppliedThreadLocal extends ThreadLocal { private final Supplier<? extends T> supplier; SuppliedThreadLocal(Supplier<? extends T> supplier) { this。supplier = Objects。requireNonNull(supplier); } @Override protected T initialValue() { return supplier。get(); }}

set()方法

設定當前執行緒的區域性變數值

原始碼中註釋的意思是:set(T value) 方法設定指定值,大多數情況下子類不需要重寫此方法,可依靠 initialValue() 方法來設定執行緒區域性變數的初始值。

//設定當前執行緒區域性變數public void set(T value) { //當前執行緒例項 Thread t = Thread。currentThread(); //拿到當前執行緒中的ThreadLocalMap ThreadLocal。ThreadLocalMap map = getMap(t); if (map != null)//存在則覆蓋 map。set(this, value); else //建立一個ThreadLocalMap,將當前執行緒t作為Map的key對應value儲存到ThreadLocalMap中 createMap(t, value);}/** * * 拿到當前執行緒中的ThreadLocalMap * * @param t 當前執行緒 * @return the map 儲存當前執行緒區域性變數副本 */ThreadLocal。ThreadLocalMap getMap(Thread t) { return t。threadLocals;}/** * 作用是給傳遞的執行緒建立一個對應的 ThreadLocalMap 並把值存進去, * 可以看到新建立的 ThreadLocalMap 被賦值給了執行緒中的 threadLocals 變數, * 這也說明對應的資料都是儲存在各個執行緒中的,所以每個執行緒對資料的操作自然不會影響其它執行緒的資料 * * @param t 當前執行緒 * @param firstValue map中的初始值 */void createMap(Thread t, T firstValue) { t。threadLocals = new ThreadLocal。ThreadLocalMap(this, firstValue);}//ThreadLocalMap 是一個定製的雜湊對映,僅適用於維護當前執行緒本地值。感興趣的同學可以看一下原始碼是怎麼實現儲存當前執行緒區域性變數的static class ThreadLocalMap { static class Entry extends WeakReference> { /** * 與此ThreadLocal關聯的值 */ Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } }}

get()方法

/** * 返回當前執行緒本地值(區域性變數的值),如果當前執行緒沒有本地值,則呼叫初始化 initialValue() 方法返回的值 * * @return 當前執行緒的本地值 */public T get() { //t:當前執行緒 Thread t = Thread。currentThread(); //map:當前執行緒區域性變數副本 ThreadLocal。ThreadLocalMap map = getMap(t); if (map != null) { //this:代表當前ThreadLocal物件,拿到ThreadLocal對應的值 ThreadLocal。ThreadLocalMap。Entry e = map。getEntry(this); if (e != null) { @SuppressWarnings(“unchecked”) T result = (T)e。value;//ThreadLocal對應的值 return result; } } return setInitialValue();//呼叫setInitialValue方法返回初始值}/** * set() 的另一種實現方式,用於建立初始值。如果使用者重寫了 set() 方法,則使用它代替 set()。 * * @return 返回初始值 */private T setInitialValue() { T value = initialValue();//如果是重寫了initialValue() 方法這裡也可以拿到初始值 Thread t = Thread。currentThread();//當前執行緒 ThreadLocal。ThreadLocalMap map = getMap(t); if (map != null) map。set(this, value); else createMap(t, value); return value;}

emove()方法

刪除當前執行緒的區域性變數值

,如果這個執行緒區域性變數隨後被當前執行緒get讀取,它的值將透過呼叫它的 initialValue() 方法重新初始化,除非它的值被當前執行緒在set時,這可能會導致在當前執行緒中多次呼叫 initialValue() 方法。

public void remove() { ThreadLocal。ThreadLocalMap m = getMap(Thread。currentThread()); if (m != null) m。remove(this);}private void remove(ThreadLocal<?> key) { ThreadLocal。ThreadLocalMap。Entry[] tab = table; int len = tab。length; int i = key。threadLocalHashCode & (len-1); for (ThreadLocal。ThreadLocalMap。Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { if (e。get() == key) { e。clear(); expungeStaleEntry(i); return; } }}//clear() GC特殊處理private T referent; /* Treated specially by GC */public void clear() { this。referent = null;}複製程式碼

實戰

業務場景

假設你的專案中有一個

UserController

類中呼叫了

userService.getUserInfo(Integer uid)

方法,此方法是用來根據使用者ID獲取使用者資訊的,此時業務需求發生了變更,需要你根據前端傳入的字串token來查詢使用者資訊,但是你發現這個方法在你的專案中已經被多個地方呼叫了,此時去更改該方法的入參結構或者是重寫一個根據token來查詢就會導致牽一髮動全身之前引用的地方也會隨之更改,有沒有一種方式能夠減少受影響的範圍,同時滿足uid和token這兩種方式來查詢呢?

前面說到 ThreadLocal 是可以打破層次間的約束,跨層傳遞物件的,且資料隔離級別是執行緒級的,我們是不是可以嘗試用 ThreadLocal 來傳遞使用者的token,話不多說,上程式碼

程式碼實現

UserController 中的get方法用來模擬查詢使用者資訊

import org。springframework。beans。factory。annotation。Autowired;import org。springframework。http。MediaType;import org。springframework。web。bind。annotation。GetMapping;import org。springframework。web。bind。annotation。RestController;@RestControllerpublic class UserController { @Autowired private IUserService userService; @GetMapping(path = “/user/get”, produces = MediaType。APPLICATION_JSON_UTF8_VALUE) public UserInfoDTO get(Integer uid) { return userService。getUserInfo(uid); }}

UserServiceImpl 實現了 IUserService 介面,完成測試資料初始化,getUserInfo實現了根據uid和token獲取使用者資訊

import lombok。extern。slf4j。Slf4j;import org。springframework。stereotype。Service;import java。util。ArrayList;import java。util。List;import java。util。Optional;@Slf4j@Servicepublic class UserServiceImpl implements IUserService { static ThreadLocal tokenStore = new ThreadLocal<>(); /** * 臨時資料,模擬儲存在資料庫表的資料 */ private List data = new ArrayList<>(); public UserServiceImpl() { initData(); } @Override public UserInfoDTO getUserInfo(Integer uid) { if (null != uid) { //模擬根據Id查詢使用者資訊 Optional optionalByUid = data。stream()。filter(u -> u。getUid()。equals(uid))。findFirst(); if (optionalByUid。isPresent()) { log。info(“根據使用者ID {} 查到了資訊”, uid); return optionalByUid。get(); } } //模擬從ThreadLocal中拿到使用者的token String token = tokenStore。get(); log。info(“UserServiceImpl 執行緒:{} 獲取了token:{}”, Thread。currentThread()。getName(), token); if (null != token) { //模擬根據token查詢使用者資訊 Optional optionalByToken = data。stream()。filter(u -> u。getToken()。equals(token))。findFirst(); if (optionalByToken。isPresent()) { return optionalByToken。get(); } } //使用者資訊不存在 return null; } /** * 生產臨時資料 */ private void initData() { this。data = new ArrayList<>(); this。data。add(new UserInfoDTO(1, “zhangsan”, “張三”, “4b86736c73674b7c910657a9c6786470”)); this。data。add(new UserInfoDTO(2, “lisi”, “里斯”, “4b86736c73674b7c910657a9c6786471”)); this。data。add(new UserInfoDTO(3, “jack”, “傑克”, “4b86736c73674b7c910657a9c6786473”)); log。info(“資料初始化完成”); }}

UserFilter 使用者過濾器用來獲取前端放在Header中的token,在UserFilter中將token放入UserServiceImpl定義的(ThreadLocal) tokenStore中完成跨層級傳遞物件

import lombok。extern。slf4j。Slf4j;import org。apache。commons。lang。StringUtils;import org。springframework。stereotype。Component;import javax。servlet。*;import javax。servlet。annotation。WebFilter;import javax。servlet。http。HttpServletRequest;import java。io。IOException;@Slf4j@WebFilter@Componentpublic class UserFilter implements Filter { @Override public void init(FilterConfig filterConfig) throws ServletException { } @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest httpServletRequest = (HttpServletRequest) request; log。info(“進入UserFilter。。。”); //獲取前端傳的token String token = httpServletRequest。getHeader(“Token”); if (StringUtils。isNotBlank(token)) { //儲存到ThreadLocal中 UserServiceImpl。tokenStore。set(token); log。info(“UserFilter 執行緒:{} 儲存了token:{}”, Thread。currentThread()。getName(), token); } chain。doFilter(request, response); } @Override public void destroy() { }}

程式碼實測

根據使用者uid查詢使用者資訊

面試官:不懂ThreadLocal,還談什麼併發程式設計?

面試官:不懂ThreadLocal,還談什麼併發程式設計?

根據token查詢使用者資訊,可以從控制檯列印中看到使用 ThreadLocal 完成了對token的跨層傳遞,也沒有影響方法原始對外的入參結構

面試官:不懂ThreadLocal,還談什麼併發程式設計?

面試官:不懂ThreadLocal,還談什麼併發程式設計?

總結

根據上面的程式碼實踐,ThreadLocal 使用起來也沒有那麼的複雜,但是在多執行緒程式設計中合理的使用 ThreadLocal 能夠讓你在工作中提高效率,程式碼更加簡潔優雅。在使用 ThreadLocal 時需要注意的點,就是可能會產生

記憶體洩露

的問題,下面這張圖將揭示了 ThreadLocal 和 Thread 以及 ThreadLocalMap 三者的關係。

面試官:不懂ThreadLocal,還談什麼併發程式設計?

在Thread類的原始碼中有一個 threadLocals,就是ThreadLocalMap

ThreadLocalMap的Entry中的key是ThreadLocal,值是我們自己設定的

ThreadLocal是一個弱引用,當為null時,會被當成垃圾被JVM回收

敲重點,如果ThreadLocal是null了,也就是要被垃圾回收器回收了,但此時ThreadLocalMap生命週期和Thread是一樣,它不會回收,這時候就出現了一個現象,ThreadLocalMap的key沒了,但是value還在,這就造成了記憶體洩漏。

解決辦法:使用完ThreadLocal後,執行remove操作,避免出現記憶體溢位情況。

問題探討:

ThreadLocal在實踐中還會遇到哪些問題?

作者:雲飛飛飛

連結:https://juejin。cn/post/7072539663185133598