用了三年 ThreadLocal 今天才弄明白其中的道理
來自:碼匠筆記
ThreadLocal對于大家并不陌生,每個人應(yīng)該多少都用過,或者接觸過,那么你真的了解她嗎?我也是今天才揭開了她的神秘面紗。
看完這篇文章你將 GET 如下知識點(diǎn):
-
什么是 ThreadLocal?
-
ThreadLocal 真的會導(dǎo)致內(nèi)存溢出嗎?
-
ThreadLocal 源碼淺析
-
ThreadLocal 最佳實(shí)踐
-
ThreadLocal.remove 解決的到底是什么問題?
ThreadLocal 是什么?
ThreadLocal字面意思是線程本地變量,那么什么是線程本地變量呢?他解決了什么問題?先看下面這個例子
public class ThreadLocalTest { public static void main(String[] args) {
Task task = new Task(); for (int i = 0; i < 3; i++) { new Thread(() -> System.out.println(Thread.currentThread().getName() + " : " + task.calc(10))).start();
}
} static class Task { private int value; public int calc(int i) {
value += i; return value;
}
}
}
內(nèi)容很簡單,啟動 3 個線程,分別調(diào)用 calc 方法,然后打印線程名字和計算內(nèi)容,輸出如下:
Thread-0 : 10
Thread-1 : 20
Thread-2 : 30
結(jié)果不難分析,因?yàn)檫@么 3 個線程共用一個 Task對象,所以 value 內(nèi)容會累加,那么結(jié)果是不是不是你預(yù)期呢?那么我們再看一個例子
public class ThreadLocalTest2 { public static void main(String[] args) {
ThreadLocalTest2.Task task = new ThreadLocalTest2.Task(); for (int i = 0; i < 3; i++) { new Thread(() -> System.out.println(Thread.currentThread().getName() + " : " + task.calc(10))).start();
}
} static class Task {
ThreadLocalvalue; public int calc(int i) {
value = new ThreadLocal();
value.set((value.get() == null ? 0 : value.get()) + i); return value.get();
}
}
}
運(yùn)行結(jié)果如下
Thread-0 : 10
Thread-1 : 10
Thread-2 : 10
這次結(jié)果就對了吧,把 value 修改成了 ThreadLocal,然后每個線程就不會互相影響內(nèi)容了,那么為什么他可以做到呢?這就是 ThreadLocal的意義所在,他解決的就是線程私有變量,多線程不互相影響。我們?nèi)ピ创a一看究竟
ThreadLocal 源碼賞析
看源碼最簡單粗暴的方式就是從入口進(jìn)行,我們直接看 ThreadLocal.set方法,她直接獲取了當(dāng)前線程,然后調(diào)用了 getMap(t),也就是當(dāng)前線程的 threadLocals變量,如果沒有直接調(diào)用 createMap創(chuàng)建,然后返回,那么看到這里我們就知道了,ThreadLocal就是一個工具類,讓我們可以把內(nèi)容通過k-v的方式設(shè)置在當(dāng)前線程上面(里面實(shí)際是使用 ThreadLocalMap進(jìn)行存儲,秒看一下代碼和 HashMap 原理非常相似),既然存儲在當(dāng)前線程上面那么當(dāng)然不會有線程安全問題了,這就是線程本地變量的內(nèi)容嘍。
當(dāng)然我們要尤為注意,key 是 this 也就是當(dāng)前的 ThreadLocal對象,記住這點(diǎn)下文要說呢。
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t); if (map != null)
map.set(this, value); else createMap(t, value);
}
ThreadLocalMap getMap(Thread t) { return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
ThreadLocal 會內(nèi)存溢出嗎?
不過還沒有結(jié)束,大家最愛談了的就是 ThreadLocal 的內(nèi)存溢出問題,那么她真的會內(nèi)存溢出么?
我們再看一個例子,例子和剛才不同的地方是只使用了一個線程(也就是 Main 線程)循環(huán)運(yùn)行示例,每次創(chuàng)建新的 Task 對象,我們可想而知,這樣每次創(chuàng)建不同的 Task,只要線程不結(jié)束,會不停的往當(dāng)前線程的 threadLocals里面 set 內(nèi)容,因?yàn)槊看味际切?Task ,自然 ThreadLocal也是新的,那么如果循環(huán)足夠大,并且線程一直存在,肯定會內(nèi)存溢出呢呀?。?!我們自己動手試試才知道啊。
下面的例子中,在 i == 80 的時候做了一次強(qiáng)制 GC,我們直接 DEBUG 看下效果。
public class ThreadLocalTest3 { public static void main(String[] args) { for (int i = 0; i < 100; i++) { new Task().calc(10); if (i == 80) {
System.gc();
}
}
} static class Task {
ThreadLocalvalue; public int calc(int i) {
value = new ThreadLocal();
value.set((value.get() == null ? 0 : value.get()) + i); return value.get();
}
}
}
在 for 循環(huán)行的左側(cè)點(diǎn)擊 debug,然后點(diǎn)擊右鍵,這樣 DEBUG 會停留在循環(huán)變量 i 等于 79 和 81 的地方,循環(huán) 100 次是為了更好的查看效果。好了我們可以直接觀察一下 i == 80 前后的運(yùn)行情況了
i == 79 || i == 81
那么開始我的表演,DEBUG 分別停在了 79 和 81 的位置上面,我們直接運(yùn)行一下當(dāng)前線程的內(nèi)容獲取到 threadLocals的內(nèi)容
Thread.currentThread().threadLocals
可以看到里面的 ThreadLocalMap 的 size 分別是 83 和 4,這說明了什么?GC的時候把 83-4 = 79 個 ThreadLocalMap的內(nèi)容回收了?
好吧,那我們繼續(xù)看下代碼吧
private ThreadLocalMap(ThreadLocalMap parentMap) {
Entry[] parentTable = parentMap.table; int len = parentTable.length;
setThreshold(len);
table = new Entry[len]; for (int j = 0; j < len; j++) { Entry e = parentTable[j]; if (e != null) { @SuppressWarnings("unchecked")
ThreadLocalkey = (ThreadLocal) e.get(); if (key != null) {
Object value = key.childValue(e.value);
Entry c = new Entry(key, value); int h = key.threadLocalHashCode & (len - 1); while (table[h] != null)
h = nextIndex(h, len);
table[h] = c;
size++;
}
}
}
}
static class Entry extends WeakReference<ThreadLocal> { /** The value associated with this ThreadLocal. */ Object value;
Entry(ThreadLocal k, Object v) { super(k);
value = v;
}
}
原來 ThreadLocal的 ThreadLocalMap里面存的每一個 Entry 是一個 WeakReference,WeakReference會在 GC 的時候進(jìn)行回收,回收的其實(shí)是 key,也就是弱引用的 referent, 然后 ThreadLocal會在 set 和 get 的時候?qū)?key 為空的 value 進(jìn)行刪除,所以這樣就完美解決了當(dāng)前線程生命周期不結(jié)束的時候,不同的 ThreadLocal不停的追加到當(dāng)前線程上面,導(dǎo)致內(nèi)存溢出。
等等,那我自己寫個程序,遇到 GC 不是就獲取不到 ThreadLocal對象了嗎?不是的,因?yàn)橐粋€對象只有僅僅被 WeakReference引用才會被回收。
哎,如果 work1 的引用不在了,并且 Entry 對 ThreadLocal 的引用是弱引用才會回收,是不是很巧妙的解決了這個問題?
所以 WeakReference解決的就是內(nèi)存溢出問題,如果持有 ThreadLocal 對象被回收了,內(nèi)存自然會被回收,如果 ThreadLocal 的對象一直存在不被回收,并不能稱之為內(nèi)存溢出。
ThreadLocal 最佳實(shí)踐
千呼萬喚始出來,因?yàn)?ThreadLocal這個特性,深受各種框架喜歡,比如 MyBatis,Spring 大量的使用的 ThreadLocal,下面是用一個最常用的案例說明一下,首先我有一個攔截器,每次請求來,使用當(dāng)前的 sl 的內(nèi)容 + 10,我是為了模擬效果,通常這個做法是用于傳遞當(dāng)前登錄態(tài),以便一次請求在任何地方都可以輕松的獲取到登錄態(tài)。
public class SessionInterceptor implements HandlerInterceptor { public static ThreadLocalsl = new ThreadLocal(); @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
Integer value = sl.get(); if (value != null) {
sl.set(value + 10);
} else {
sl.set(10);
} return true;
} @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView) throws Exception {
} @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception {
}
}
然后我在 controller 里面獲取 ThreadLocal里面的內(nèi)容,并打印當(dāng)前線程的名稱和內(nèi)容
@RestController public class IndexController { @RequestMapping("/") public Integer test() {
System.out.println(Thread.currentThread().getName() + " : " + SessionInterceptor.sl.get()); return SessionInterceptor.sl.get();
}
}
接下來我們啟動服務(wù),運(yùn)行我編寫好的 Spring Boot Application
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class}) public class MainBootstrap { public static void main(String[] args) {
SpringApplication.run(MainBootstrap.class, args);
}
}
瀏覽器訪問 https://localhost:8080,瘋狂的刷新瀏覽器,控制臺打印的效果如下
http-nio-8080-exec-1 : 10
http-nio-8080-exec-3 : 10
http-nio-8080-exec-4 : 10
http-nio-8080-exec-1 : 20
http-nio-8080-exec-2 : 10
http-nio-8080-exec-3 : 20
http-nio-8080-exec-4 : 20
http-nio-8080-exec-1 : 30
http-nio-8080-exec-2 : 20
http-nio-8080-exec-3 : 30
http-nio-8080-exec-4 : 30
呀,和我想象的不一樣啊,我這可是瀏覽器的請求,不應(yīng)該是每個請求一個線程,使用自己的 ThreadLocal 嗎,怎么值也累加了?
別慌問題出現(xiàn)在這里,在池化技術(shù)流行的年代,自然 Tomcat 也用了池化基礎(chǔ),其實(shí)每個請求過來,是直接在 Tomcat 的線程池里面獲取一個線程,然后運(yùn)行,所以一個請求結(jié)束如果 ThreadLocal 的內(nèi)容不重置,就會影響其他請求,想象如果你這個地方是做的用戶登錄的綁定,那么豈不是資源亂套了?
那么怎么解決呢?還記得剛才的 SessionInterceptor 類么,直接在里面的 afterCompletion添加 sl.remove()即可,意思是在請求結(jié)束的時候,把當(dāng)前線程的私有變量刪除,這樣就不影響其他線程了。
網(wǎng)上的一些說這個操作是為了更好的 GC 回收沒用的實(shí)例,如果不設(shè)置也會自動回收,其實(shí)是不對的。為了讓上下文都可以獲取到 ThreadLocal 的內(nèi)容,所以比如是靜態(tài)的 ThreadLocal 所以持有的引用一直存在,并不會被回收,所以其實(shí)是在恢復(fù)線程的狀態(tài),不影響其他請求。
@Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception {
sl.remove();
}
修改以后我們重新狂刷瀏覽器,是不是問題就解決了呢?好的如果你有任何關(guān)于 ThreadLocal 的問題歡迎給我留言其他討論,如果有不對的地方也歡迎指正。對了所有文章中的代碼,都可以在訂閱號后臺回復(fù) “ThreadLocal” 獲取。
特別推薦一個分享架構(gòu)+算法的優(yōu)質(zhì)內(nèi)容,還沒關(guān)注的小伙伴,可以長按關(guān)注一下:
長按訂閱更多精彩▼
如有收獲,點(diǎn)個在看,誠摯感謝
免責(zé)聲明:本文內(nèi)容由21ic獲得授權(quán)后發(fā)布,版權(quán)歸原作者所有,本平臺僅提供信息存儲服務(wù)。文章僅代表作者個人觀點(diǎn),不代表本平臺立場,如有問題,請聯(lián)系我們,謝謝!