說明
接著上次手撕面試題ThreadLocal!!!面試官一聽,哎呦不錯哦!本文將繼續上文的話題,來聊聊FastThreadLocal,目前關於FastThreadLocal的很多文章都有點老有點過時了(本文將澄清幾個誤區),很多文章關於FastThreadLocal介紹的也不全,希望本篇文章可以帶你徹底理解FastThreadLocal!!!
FastThreadLocal是Netty提供的,在池化記憶體分配等都有涉及到!
關於FastThreadLocal,零度準備從這幾個方面進行講解:
- FastThreadLocal的使用。
- FastThreadLocal並不是什麼情況都快,你要用對才會快。
- FastThreadLocal利用位元組填充來解決偽共享問題。
- FastThreadLocal比ThreadLocal快,並不是空間換時間。
- FastThreadLocal不在使用ObjectCleaner處理洩漏,必要的時候建議重寫onRemoval方法。
- FastThreadLocal為什麼快?
FastThreadLocal的使用
FastThreadLocal用法上相容ThreadLocal
FastThreadLocal使用示例程式碼:
public class FastThreadLocalTest {
private static FastThreadLocal<Integer> fastThreadLocal = new FastThreadLocal<>();
public static void main(String[] args) {
//if (thread instanceof FastThreadLocalThread) 使用FastThreadLocalThread更優,普通執行緒也可以
new FastThreadLocalThread(() -> {
for (int i = 0; i < 100; i++) {
fastThreadLocal.set(i);
System.out.println(Thread.currentThread().getName() + "====" + fastThreadLocal.get());
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "fastThreadLocal1").start();
new FastThreadLocalThread(() -> {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + "====" + fastThreadLocal.get());
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "fastThreadLocal2").start();
}
}
程式碼截圖:
程式碼執行結果:
我們在回顧下之前的ThreadLocal的 最佳實踐做法:
try {
// 其它業務邏輯
} finally {
threadLocal物件.remove();
}
備註: 通過上面的例子,我們發現FastThreadLocal和ThreadLocal在用法上面基本差不多,沒有什麼特別區別,個人認為,這就是FastThreadLocal成功的地方,它就是要讓使用者用起來和ThreadLocal沒啥區別,要相容!
使用FastThreadLocal居然不用像ThreadLocal那樣先try ………………… 之後finally進行threadLocal物件.remove();
由於構造FastThreadLocalThread的時候,通過FastThreadLocalRunnable對Runnable物件進行了包裝:
FastThreadLocalRunnable.wrap(target)從而構造了FastThreadLocalRunnable物件。
FastThreadLocalRunnable在執行完之後都會呼叫FastThreadLocal.removeAll();
備註: FastThreadLocal不在使用ObjectCleaner處理洩漏,必要的時候建議重寫onRemoval方法。關於這塊將在本文後面進行介紹,這樣是很多網上資料比較老的原因,這塊已經去掉了。
如果是普通執行緒,還是應該最佳實踐:
finally {
fastThreadLocal物件.removeAll();
}注意: 如果使用FastThreadLocal就不要使用普通執行緒,而應該構建FastThreadLocalThread,關於為什麼這樣,關於這塊將在本文後面進行介紹:FastThreadLocal並不是什麼情況都快,你要用對才會快。
FastThreadLocal並不是什麼情況都快,你要用對才會快
首先看看netty關於這塊的測試用例:
程式碼路徑:https://github.com/netty/netty/blob/4.1/microbench/src/main/java/io/netty/microbench/concurrent/FastThreadLocalFastPathBenchmark.java
備註: 在我本地進行測試,FastThreadLocal的吞吐量是jdkThreadLocal的3倍左右。機器不一樣,可能效果也不一樣,大家可以自己試試,反正就是快了不少。
關於ThreadLocal,之前的這篇:手撕面試題ThreadLocal!!!已經詳細介紹了。
FastThreadLocal並不是什麼情況都快,你要用對才會快!!!
注意: 使用FastThreadLocalThread執行緒才會快,如果是普通執行緒還更慢!
注意: 使用FastThreadLocalThread執行緒才會快,如果是普通執行緒還更慢!
注意: 使用FastThreadLocalThread執行緒才會快,如果是普通執行緒還更慢!
netty的測試目錄下面有2個類:
- FastThreadLocalFastPathBenchmark
- FastThreadLocalSlowPathBenchmark
路徑:https://github.com/netty/netty/blob/4.1/microbench/src/main/java/io/netty/microbench/concurrent/
FastThreadLocalFastPathBenchmark測試結果: 是ThreadLocal的吞吐量的3倍左右。
FastThreadLocalSlowPathBenchmark測試結果: 比ThreadLocal的吞吐量還低。
測試結論: 使用FastThreadLocalThread執行緒操作FastThreadLocal才會快,如果是普通執行緒還更慢!
註釋裡面給出了三點:
FastThreadLocal操作元素的時候,使用常量下標在陣列中進行定位元素來替代ThreadLocal通過雜湊和雜湊表,這個改動特別在頻繁使用的時候,效果更加顯著!
想要利用上面的特徵,執行緒必須是FastThreadLocalThread或者其子類,預設DefaultThreadFactory都是使用FastThreadLocalThread的
只用在FastThreadLocalThread或者子類的執行緒使用FastThreadLocal才會更快,因為FastThreadLocalThread 定義了屬性threadLocalMap型別是InternalThreadLocalMap。如果普通執行緒會藉助ThreadLocal。
我們看看NioEventLoopGroup細節:
看到這裡,和剛剛我們看到的註釋內容一致的,是使用FastThreadLocalThread的。
netty裡面使用FastThreadLocal的舉例常用的:
池化記憶體分配:
會使用到Recycler
而Recycler也使用了FastThreadLocal
我們再看看看測試類:
備註: 我們會發現FastThreadLocalFastPathBenchmark裡面的執行緒是FastThreadLocal。
備註: 我們會發現FastThreadLocalSlowPathBenchmark裡面的執行緒 不是FastThreadLocal。
FastThreadLocal只有被的執行緒是FastThreadLocalThread或者其子類使用的時候才會更快,吞吐量我這邊測試的效果大概3倍左右,但是如果是普通執行緒操作FastThreadLocal其吞吐量比ThreadLocal還差!
FastThreadLocal利用位元組填充來解決偽共享問題
關於CPU 快取 內容來源於美團:https://tech.meituan.com/2016/11/18/disruptor.html
下圖是計算的基本結構。L1、L2、L3分別表示一級快取、二級快取、三級快取,越靠近CPU的快取,速度越快,容量也越小。所以L1快取很小但很快,並且緊靠著在使用它的CPU核心;L2大一些,也慢一些,並且仍然只能被一個單獨的CPU核使用;L3更大、更慢,並且被單個插槽上的所有CPU核共享;最後是主存,由全部插槽上的所有CPU核共享。
當CPU執行運算的時候,它先去L1查詢所需的資料、再去L2、然後是L3,如果最後這些快取中都沒有,所需的資料就要去主記憶體拿。走得越遠,運算耗費的時間就越長。所以如果你在做一些很頻繁的事,你要儘量確保資料在L1快取中。
另外,執行緒之間共享一份資料的時候,需要一個執行緒把資料寫回主存,而另一個執行緒訪問主存中相應的資料。
下面是從CPU訪問不同層級資料的時間概念:
可見CPU讀取主存中的資料會比從L1中讀取慢了近2個數量級。
快取行
Cache是由很多個cache line組成的。每個cache line通常是64位元組,並且它有效地引用主記憶體中的一塊兒地址。一個Java的long型別變數是8位元組,因此在一個快取行中可以存8個long型別的變數。
CPU每次從主存中拉取資料時,會把相鄰的資料也存入同一個cache line。
在訪問一個long陣列的時候,如果陣列中的一個值被載入到快取中,它會自動載入另外7個。因此你能非常快的遍歷這個陣列。事實上,你可以非常快速的遍歷在連續記憶體塊中分配的任意資料結構。
偽共享
由於多個執行緒同時操作同一快取行的不同變數,但是這些變數之間卻沒有啥關聯,但是每次修改,都會導致快取的資料變成無效,從而明明沒有任何修改的內容,還是需要去主存中讀(CPU讀取主存中的資料會比從L1中讀取慢了近2個數量級)但是其實這塊內容並沒有任何變化,由於快取的最小單位是一個快取行,這就是偽共享。
如果讓多執行緒頻繁操作的並且沒有關係的變數在不同的快取行中,那麼就不會因為快取行的問題導致沒有關係的變數的修改去影響另外沒有修改的變數去讀主存了(那麼從L1中取是從主存取快2個數量級的)那麼效能就會好很多很多。
有偽共享 和沒有的情況的測試效果
程式碼路徑:https://github.com/jiangxinlingdu/nettydemo
利用位元組填充來解決偽共享,從而速度快了3倍左右。
FastThreadLocal使用位元組填充解決偽共享
之前介紹ThreadLocal的時候,說過ThreadLocal是用在多執行緒場景下,那麼FastThreadLocal也是用在多執行緒場景,大家可以看下這篇:手撕面試題ThreadLocal!!!,所以FastThreadLocal需要解決偽共享問題,FastThreadLocal使用位元組填充解決偽共享。
這個是我自己手算的,通過手算太麻煩,推薦一個工具JOL。
http://openjdk.java.net/projects/code-tools/jol/
推薦IDEA外掛:https://plugins.jetbrains.com/plugin/10953-jol-java-object-layout
程式碼路徑:https://github.com/jiangxinlingdu/nettydemo
通過這個工具算起來就很容易了,如果以後有類似的需要看的,不用手一個一個算了。
FastThreadLocal被FastThreadLocalThread進行讀寫的時候也可能利用到快取行
並且由於當執行緒是FastThreadLocalThread的時候操作FastThreadLocal是通過indexedVariables陣列進行儲存資料的的,每個FastThreadLocal有一個常量下標,通過下標直接定位陣列進行讀寫操作,當有很多FastThreadLocal的時候,也可以利用快取行,比如一次indexedVariables陣列第3個位置資料,由於快取的最小單位是快取行,順便把後面的4、5、6等也快取了,下次剛剛好另外FastThreadLocal下標就是5的時候,進行讀取的時候就直接走快取了,比走主存可能快2個數量級。
一點疑惑
問題:為什麼這裡填充了9個long值呢???
我提了一個issue:https://github.com/netty/netty/issues/9284
雖然也有人回答,但是感覺不是自己想要的,說服不了自己!!!
FastThreadLocal比ThreadLocal快,並不是空間換時間
現在清理已經去掉,本文下面會介紹,所以FastThreadLocal比ThreadLocal快,並不是空間換時間,FastThreadLocal並沒有浪費空間!!!
FastThreadLocal不在使用ObjectCleaner處理洩漏,必要的時候建議重寫onRemoval方法
最新的netty版本中已經不在使用ObjectCleaner處理洩漏:
去掉原因:
https://github.com/netty/netty/issues/8017
我們看看FastThreadLocal的onRemoval
如果使用的是FastThreadLocalThread能保證呼叫的,重寫onRemoval做一些收尾狀態修改等等
FastThreadLocal為什麼快?
FastThreadLocal操作元素的時候,使用常量下標在陣列中進行定位元素來替代ThreadLocal通過雜湊和雜湊表,這個改動特別在頻繁使用的時候,效果更加顯著!計算該ThreadLocal需要儲存的位置是通過hash演算法確定位置:
int i = key.threadLocalHashCode & (len-1);而FastThreadLocal就是一個常量下標index,這個如果執行次數很多也是有影響的。
並且FastThreadLocal利用快取行的特性,FastThreadLocal是通過indexedVariables陣列進行儲存資料的,如果有多個FastThreadLocal的時候,也可以利用快取行,比如一次indexedVariables陣列第3個位置資料,由於快取的最小單位是快取行,順便把後面的4、5、6等也快取了,下次剛剛好改執行緒需要讀取另外的FastThreadLocal,這個FastThreadLocal的下標就是5的時候,進行讀取的時候就直接走快取了,比走主存可能快2個數量級而ThreadLocal通過hash是分散的。
如果讀完覺得有收穫的話,歡迎點贊、關注、加公眾號 [匠心零度] ,查閱更多精彩歷史!!!