ThreadLocal與執行緒池
問題
Web專案,使用者每次登入系統會將Session資訊儲存到redis中,然後返回給客戶端token,該token作為使用者會話的標示,此後所有的請求都會走一個攔截器,此攔截器根據token將Session資訊從redis中拿回來放到ThreadLocal中。當併發較多時,會出現Session資訊錯誤的情況。
經分析出錯的流程是:使用者1登入系統並訪問系統此時使用的是執行緒A並將使用者1的Session資訊存到執行緒A的TheadLocal,然後一個沒有登入使用者2也訪問了系統,伺服器再次分配執行緒A來處理此請求,此時使用者2取到了使用者1的Session資訊。
ThreadLocal
TheadLocal是jdk提供的一個很好用的執行緒內共享變數工具。在Web開發時,伺服器端可以使用TheadLocal來儲存請求的Request、Response。不用顯示的在方法呼叫棧傳遞。能很方便的線上程內方法呼叫棧的任何地方獲取我們存進的例項變數。
class WebContext {
private TheadLocal<HttpServletRequest> request = new TheadLocal<>();
private TheadLocal<HttpServletResponse> response = new TheadLocal<>();
public static void setRequest(HttpServletRequest req) {
request.set(req);
}
public static HttpServletRequest getRequest() {
request.get();
}
public static void setResponse(HttpServletResponse resp) {
response.set(resp);
}
public static HttpServletResponse getResponse() {
response.get();
}
}
class AbcController {
public Object userInfo() {
HttpServletRequest request = WebContext.getRequest();
// request.getParameter()
// ...
}
}
複製程式碼
看下TheadLocal的程式碼:
// TheadLocal兩個重要的方法
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this,value);
else
createMap(t,value);
}
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();
}
複製程式碼
TheadLocal不管是set方法還是get方法,都會先Thread t = Thread.currentThread();
,獲取當前執行緒例項。然後獲取執行緒例項中的TheadLocalMap例項ThreadLocalMap map = getMap(t);
這也是為什麼在同一個執行緒中獲取執行緒變數得到的會是同一個例項,由於每一個執行緒例項都儲存自己的執行緒變數所以,這些變數是執行緒安全的。
public class Thread implements Runnable {
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
}
複製程式碼
以上是Thread類裡宣告的threadLocals
屬性。可以看出實際儲存執行緒變數的是ThreadLocal.ThreadLocalMap
類,這個類和Map類似儲存鍵值對(Key—Value),其中Key就是執行緒例項,Value就是執行緒變數。
Tomcat執行緒池
由於作業系統對執行緒的建立是很消耗資源的,所以很多應用會採用執行緒池的方式,系統初始化是一次性建立一定量的,這些執行緒就會複用共享,也就是說執行緒使用完之後是不會被銷燬的,直到應用停止。
這樣導致的結果就是,不同使用者請求複用相同的執行緒導致,不同使用者取得的Session資訊相同,導致Session資訊錯亂。
解決
在一個設定一個攔截器,在請求的前端
設定Session資訊到本地執行緒變數,在後端
清空本地執行緒變數Session資訊。
// 程式碼簡化了
public class SessionInspector extends HandlerInterceptorAdapter {
@Override
public boolean preHandle(...) throws Exception {
// ThreadLocal.set(Session)
return true;
}
public void afterCompletion(...) {
// ThreadLocal.clear()
}
}
複製程式碼
總結
ThreadLocal為我們提供了方便的執行緒內共享變數的方式,提供了一種保證資料執行緒安全的方式。但是要考慮在多執行緒環境中執行緒複用的情況,如果ThreadLocal
內儲存資訊是會話等與使用者相關的資訊要線上程任務結束時清理ThreadLocal
內的資訊。