接着上次手撕面試題ThreadLocal!!!面試官一聽,哎呦不錯哦!本文將繼續上文的話題,來聊聊FastThreadLocal,目前關於FastThreadLocal的不少文章都有點老有點過期了(本文將澄清幾個誤區),不少文章關於FastThreadLocal介紹的也不全,但願本篇文章能夠帶你完全理解FastThreadLocal!!!html
FastThreadLocal是Netty提供的,在池化內存分配等都有涉及到!java
關於FastThreadLocal,零度準備從這幾個方面進行講解:git
FastThreadLocal用法上兼容ThreadLocalgithub
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沒啥區別,要兼容!bash
使用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並非什麼狀況都快,你要用對纔會快。
首先看看netty關於這塊的測試用例: 代碼路徑:github.com/netty/netty…
備註: 在我本地進行測試,FastThreadLocal的吞吐量是jdkThreadLocal的3倍左右。機器不同,可能效果也不同,你們能夠本身試試,反正就是快了很多。
關於ThreadLocal,以前的這篇:手撕面試題ThreadLocal!!!已經詳細介紹了。
FastThreadLocal並非什麼狀況都快,你要用對纔會快!!!
注意: 使用FastThreadLocalThread線程纔會快,若是是普通線程還更慢! 注意: 使用FastThreadLocalThread線程纔會快,若是是普通線程還更慢! 注意: 使用FastThreadLocalThread線程纔會快,若是是普通線程還更慢!
netty的測試目錄下面有2個類:
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還差!
關於CPU 緩存 內容來源於美團:tech.meituan.com/2016/11/18/…
下圖是計算的基本結構。L一、L二、L3分別表示一級緩存、二級緩存、三級緩存,越靠近CPU的緩存,速度越快,容量也越小。因此L1緩存很小但很快,而且緊靠着在使用它的CPU內核;L2大一些,也慢一些,而且仍然只能被一個單獨的CPU核使用;L3更大、更慢,而且被單個插槽上的全部CPU核共享;最後是主存,由所有插槽上的全部CPU核共享。
當CPU執行運算的時候,它先去L1查找所需的數據、再去L二、而後是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個數量級的)那麼性能就會好不少不少。
利用字節填充來解決僞共享,從而速度快了3倍左右。
以前介紹ThreadLocal的時候,說過ThreadLocal是用在多線程場景下,那麼FastThreadLocal也是用在多線程場景,你們能夠看下這篇:手撕面試題ThreadLocal!!!,因此FastThreadLocal須要解決僞共享問題,FastThreadLocal使用字節填充解決僞共享。
這個是我本身手算的,經過手算太麻煩,推薦一個工具JOL。
推薦IDEA插件:plugins.jetbrains.com/plugin/1095…
經過這個工具算起來就很容易了,若是之後有相似的須要看的,不用手一個一個算了。
而且因爲當線程是FastThreadLocalThread的時候操做FastThreadLocal是經過indexedVariables數組進行存儲數據的的,每一個FastThreadLocal有一個常量下標,經過下標直接定位數組進行讀寫操做,當有不少FastThreadLocal的時候,也能夠利用緩存行,好比一次indexedVariables數組第3個位置數據,因爲緩存的最小單位是緩存行,順便把後面的四、五、6等也緩存了,下次剛恰好另外FastThreadLocal下標就是5的時候,進行讀取的時候就直接走緩存了,比走主存可能快2個數量級。
問題: 爲何這裏填充了9個long值呢???
我提了一個issue:github.com/netty/netty…
雖然也有人回答,可是感受不是本身想要的,說服不了本身!!!
如今清理已經去掉,本文下面會介紹,因此FastThreadLocal比ThreadLocal快,並非空間換時間,FastThreadLocal並無浪費空間!!!
最新的netty版本中已經不在使用ObjectCleaner處理泄漏:
去掉緣由:
咱們看看FastThreadLocal的onRemoval
若是使用的是FastThreadLocalThread能保證調用的,重寫onRemoval作一些收尾狀態修改等等
FastThreadLocal操做元素的時候,使用常量下標在數組中進行定位元素來替代ThreadLocal經過哈希和哈希表,這個改動特別在頻繁使用的時候,效果更加顯著!計算該ThreadLocal須要存儲的位置是經過hash算法肯定位置: int i = key.threadLocalHashCode & (len-1);而FastThreadLocal就是一個常量下標index,這個若是執行次數不少也是有影響的。
而且FastThreadLocal利用緩存行的特性,FastThreadLocal是經過indexedVariables數組進行存儲數據的,若是有多個FastThreadLocal的時候,也能夠利用緩存行,好比一次indexedVariables數組第3個位置數據,因爲緩存的最小單位是緩存行,順便把後面的四、五、6等也緩存了,下次剛恰好改線程須要讀取另外的FastThreadLocal,這個FastThreadLocal的下標就是5的時候,進行讀取的時候就直接走緩存了,比走主存可能快2個數量級而ThreadLocal經過hash是分散的。
若是讀完以爲有收穫的話,歡迎點贊、關注、加公衆號【匠心零度】,查閱更多精彩歷史!!!