深入理解Java多線程——ThreadLocal

定義

ThreadLocal是線程局部變量,不同線程的threadlocal相互獨立。它是一種保存線程私有信息的機制,因爲在現成的整個生命週期都有效,
所以可以方便地在一個線程關聯的不同業務模塊之間傳遞信息,比如事務ID、Cookie等上下文相關信息。
1488206-20200309144911317-261957878.png

特點:

  • 簡單,開箱即用。
  • 快速,無額外開銷。
  • 安全,線程安全。

    threadlocal設計實現的優點:開箱即用,代碼易讀,符合經常閱讀偶爾使用的原則

場景:資源持有、線程一致性、併發計算等多線程場景。

API

1488206-20200309145324977-1032106601.png

場景分析

1488206-20200309145451092-952331564.png

1488206-20200309145645857-1946961591.png
如jdbc事務的請求,part i 代表不同事務,如更新用戶信息、更新訂單信息等,需要保證各事務資源的一致性。
每一個事務請求連接時,先去threadlocal map裏找,如果不存在,到連接池中請求分配連接,並存入map。

1488206-20200309150053862-1505873241.png

1488206-20200309150125168-1312384883.png

總結,ThreadLocal的主要作用是:

  • 持有線程資源,供線程的各個部分使用
  • 幫助維護多線程共享資源的一致性
  • 線程安全的一種方案

場景實驗,觀察Spring框架在多線程場景的執行情況

壓力測試工具,apache2-util

10000此請求,單線程

1488206-20200309150618783-54595491.png

1488206-20200309150907739-2041913640.png

1488206-20200309150912563-1866595032.png

1488206-20200309150921241-837905550.png

10000次請求,線程數加到100

1488206-20200309150957484-2004894405.png
雖然完成10000請求的速度變快了,但最終curl請求get到的數據會不一致。
原因是,多線程下,c是臨界資源,c=c+1不具備原子性,要先讀取c,在執行加1,再執行賦值。
1488206-20200309151130989-1447585817.png

對c的訪問加鎖

1488206-20200309151838188-73848559.png
測試發現curl get到的結果是10000,但速度會很慢,原因是加鎖導致排隊,併發實質上變成了串行,性能被鎖卡住。
解決方法就是使用ThreadLocal,讓線程在自己的局部變量資源上運行。

把c設爲ThreadLocal

1488206-20200309152150312-1761865136.png
測試發現,最終統計數據依然不準確。
1488206-20200309152348626-1897492628.png
原因是spring默認線程池有20多個線程,這些線程每一個都有自己的局部變量c,執行10000請求後,需要收集各個線程的數據。

收集多個ThreadLocal中的數據

雖然threadlocal是各個線程獨佔的數據,但也是進程持有的,不過java沒有提供收集數據的接口,所以可以通過hashmap或
hashset來存儲threadlocal,最後一併收集。
1488206-20200309152709365-816214302.png
1488206-20200309152719894-1814613504.png
1488206-20200309152728147-1368110800.png
改進,因爲set訪問需要同步,所以addset中加入同步鎖,而且set訪問次數最多是線程池的線程數,相對c的訪問次數要少,
屬於低頻訪問,所以對總體性能影響小。
1488206-20200309152742435-1580697088.png

實驗總結

  • 基於線程池模型加同步鎖很危險,可能因排隊等鎖,導致cpu使用不充分,從而嚴重拖慢性能。
  • 雖然多線程不能完全避免同步問題,但使用ThreadLocal,可以把高頻同步化爲低頻同步。

實現原理

自定義HashMap存放ThreadLocal弱引用,延續了lazy-load模式,初始容量16,門限2/3

satic class ThreadLocalMap {
 satic class Entry extends WeakReference<ThreadLocal<?>> {
 /** The value associated with this ThreadLocal. */
 Object value;
 Entry(ThreadLocal<?> k, Object v) {
 super(k);
 value = v;
 }
 }
 // …
}

回收被內存回收的弱引用所佔的槽是超級複雜問題。當Key爲null時,該條目就變成“廢棄條目”,
相關“value”的回收,往往依賴於幾個關鍵點,即set、remove、rehash。

下面set的精簡代碼,具體的清理邏輯是實現在cleanSomeSlots和expungeStaleEntry之中。

private void set(ThreadLocal<?> key, Object value) {
 Entry[] tab = table;
 int len = tab.length;
 int i = key.threadLocalHashCode & (len-1);
 for (Entry e = tab[i];; …) {
 //…
 if (k == null) {
// 替換廢棄條目
 replaceStaleEntry(key, value, i);
 return;
 }
 }
 tab[i] = new Entry(key, value);
 int sz = ++size;
// 掃描並清理髮現的廢棄條目,並檢查容量是否超限
 if (!cleanSomeSlots(i, sz) && sz >= threshold)
 rehash();// 清理廢棄條目,如果仍然超限,則擴容(加倍)
}

廢棄項目的回收依賴於顯式地觸發,否則就要等待線程結束,進而回收相應ThreadLocalMap!這就是很多OOM的來源,
所以應用一定要自己負 責remove,並且不要和線程池配合,因爲worker線程往往是不會退出的。

hash算法

散列更均勻,減少衝突
1488206-20200309153744242-574755069.png
解決衝突的方法是後移法。

總結

解決一致性問題,除了排隊(加鎖)、投票(拜占庭將軍)、CAS+voilate外,ThreadLocal不失爲一個更輕量級的優選方案。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章