【併發程式設計】ThreadLocal其實很簡單
什麼是ThreadLocal
ThreadLocal有點類似於Map型別的資料變數。ThreadLocal型別的變數每個執行緒都有自己的一個副本,某個執行緒對這個變數的修改不會影響其他執行緒副本的值。需要注意的是一個ThreadLocal變數,其中只能set一個值。
ThreadLocal<String> localName = new ThreadLocal();
localName.set("name1");
String name = localName.get();
線上程1中初始化了一個ThreadLocal物件localName,並通過set方法,儲存了一個值,同時線上程1中通過 localName.get()可以拿到之前設定的值,但是如果線上程2中,拿到的將是一個null。
下面來看下ThreadLocal的原始碼:
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(); }
可以發現,每個執行緒中都有一個 ThreadLocalMap資料結構,當執行set方法時,其值是儲存在當前執行緒的 threadLocals變數中,當執行get方法中,是從當前執行緒的 threadLocals變數獲取。 (ThreadLocalMap的key值是ThreadLocal型別)
所以線上程1中set的值,對執行緒2來說是摸不到的,而且線上程2中重新set的話,也不會影響到執行緒1中的值,保證了執行緒之間不會相互干擾。
上面提到ThreadLoal的變數都是儲存在ThreadLoalMap的變數中,下面給出下Thread、ThreadLoal和ThreadLoalMap的關係。
Thread類有屬性變數threadLocals (型別是ThreadLocal.ThreadLocalMap),也就是說每個執行緒有一個自己的ThreadLocalMap ,所以每個執行緒往這個ThreadLocal中讀寫隔離的,並且是互相不會影響的。一個ThreadLocal只能儲存一個Object物件,如果需要儲存多個Object物件那麼就需要多個ThreadLocal!
ThreadLocal使用場景
說完ThreadLocal的原理,我們來看看ThreadLocal的使用場景。
1. 儲存執行緒上下文資訊,在任意需要的地方可以獲取
比如我們在使用Spring MVC時,想要在Service層使用HttpServletRequest。一種方式就是在Controller層將這個變數傳給Service層,但是這種寫法不夠優雅。Spring早就幫我們想到了這種情況,而且提供了現成的工具類:
public static final HttpServletRequest getRequest(){
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
return request;
}
public static final HttpServletResponse getResponse(){
HttpServletResponse response = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse();
return response;
}
上面的程式碼就是使用ThreadLocal實現變數線上程各處傳遞的。
2. 保證某些情況下的執行緒安全,提升效能
效能監控,如記錄一下請求的處理時間,得到一些慢請求(如處理時間超過500毫秒),從而進行效能改進。這邊我們以Spring MVC的攔截器功能為列子。
public class StopWatchHandlerInterceptor extends HandlerInterceptorAdapter {
//NamedThreadLocal是Spring對ThreadLocal的封裝,原理一樣
//在多執行緒情況下,startTimeThreadLocal變數必須每個執行緒之間隔離
private NamedThreadLocal<Long> startTimeThreadLocal = new NamedThreadLocal<Long>("StopWatch-StartTime");
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,Object handler) throws Exception {
//1、開始時間
long beginTime = System.currentTimeMillis();
//執行緒繫結變數(該資料只有當前請求的執行緒可見)
startTimeThreadLocal.set(beginTime);
//繼續流程
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response,Object handler, Exception ex) throws Exception {
long endTime = System.currentTimeMillis();//2、結束時間
long beginTime = startTimeThreadLocal.get();//得到執行緒繫結的區域性變數(開始時間)
long consumeTime = endTime - beginTime;//3、消耗的時間
if(consumeTime > 500) {//此處認為處理時間超過500毫秒的請求為慢請求
//TODO 記錄到日誌檔案
System.out.println(String.format("%s consume %d millis", request.getRequestURI(), consumeTime));
}
}
}
說明:其實要實現上面的功能,完全可以不用ThreadLocal(同步鎖等),但是上面的程式碼的確是說明ThreadLocal這個是用場景很好的列子。
ThreadLocal的最佳實踐
從上面的圖中可以看到,Entry的key指向ThreadLocal用虛線表示弱引用 ,下面我們來看看ThreadLocalMap:
java物件的引用包括 : 強引用,軟引用,弱引用,虛引用 。
弱引用也是用來描述非必需物件的,當JVM進行垃圾回收時,無論記憶體是否充足,該物件僅僅被弱引用關聯,那麼就會被回收。當僅僅只有ThreadLocalMap中的Entry的key指向ThreadLocal的時候,ThreadLocal會進行回收的!!!
ThreadLocal被垃圾回收後,在ThreadLocalMap裡對應的Entry的鍵值會變成null,但是Entry是強引用,那麼Entry裡面儲存的Object,並沒有辦法進行回收,所以ThreadLocalMap 存在記憶體洩露的風險。
所以最佳實踐,應該在我們不使用的時候,主動呼叫remove方法進行清理。這裡給出一個建議方案:
public class Dynamicxx {
private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();
public void dosomething(){
try {
contextHolder.set("name");
// 其它業務邏輯
} finally {
contextHolder .remove();
}
}
}
參考
- https://mp.weixin.qq.com/s/1ccG1R3ccP0_5A7A5zCdzQ
- https://mp.weixin.qq.com/s/SNLNJcap8qmJF9r4IuY8LA