FastThreadLocal吞吐量居然是ThreadLocal的3倍
目前關(guān)于FastThreadLocal的很多文章都有點老有點過時了(本文將澄清幾個誤區(qū)),很多文章關(guān)于FastThreadLocal介紹的也不全,希望本篇文章可以帶你徹底理解FastThreadLocal!!!
FastThreadLocal是Netty提供的,在池化內(nèi)存分配等都有涉及到!
關(guān)于FastThreadLocal,零度準(zhǔn)備從這幾個方面進(jìn)行講解:
- FastThreadLocal的使用。
- FastThreadLocal并不是什么情況都快,你要用對才會快。
- FastThreadLocal利用字節(jié)填充來解決偽共享問題。
- FastThreadLocal比ThreadLocal快,并不是空間換時間。
- FastThreadLocal不在使用ObjectCleaner處理泄漏,必要的時候建議重寫onRemoval方法。
- FastThreadLocal為什么快?
FastThreadLocal的使用
FastThreadLocal用法上兼容ThreadLocal
FastThreadLocal使用示例代碼截圖:
?
?
?
代碼運行結(jié)果:
?
?
?
我們在回顧下之前的ThreadLocal的最佳實踐做法:
try {
// 其它業(yè)務(wù)邏輯
} finally {
threadLocal對象.remove();
} ?
備注:通過上面的例子,我們發(fā)現(xiàn)FastThreadLocal和ThreadLocal在用法上面基本差不多,沒有什么特別區(qū)別,個人認(rèn)為,這就是FastThreadLocal成功的地方,它就是要讓用戶用起來和ThreadLocal沒啥區(qū)別,要兼容!
使用FastThreadLocal居然不用像ThreadLocal那樣先try ………………… 之后finally進(jìn)行threadLocal對象.remove();
由于構(gòu)造FastThreadLocalThread的時候,通過FastThreadLocalRunnable對Runnable對象進(jìn)行了包裝:
FastThreadLocalRunnable.wrap(target)
從而構(gòu)造了FastThreadLocalRunnable對象。
?
?
?
FastThreadLocalRunnable在執(zhí)行完之后都會調(diào)用FastThreadLocal.removeAll();
?
?
?
備注: FastThreadLocal不在使用ObjectCleaner處理泄漏,必要的時候建議重寫onRemoval方法。關(guān)于這塊將在本文后面進(jìn)行介紹,這樣是很多網(wǎng)上資料比較老的原因,這塊已經(jīng)去掉了。
如果是普通線程,還是應(yīng)該最佳實踐:
finally {
fastThreadLocal對象.removeAll();
}
注意: 如果使用FastThreadLocal就不要使用普通線程,而應(yīng)該構(gòu)建FastThreadLocalThread,關(guān)于為什么這樣,關(guān)于這塊將在本文后面進(jìn)行介紹:FastThreadLocal并不是什么情況都快,你要用對才會快。
FastThreadLocal并不是什么情況都快,你要用對才會快
首先看看netty關(guān)于這塊的測試用例:
代碼路徑:https://github.com/netty/netty/blob/4.1/microbench/src/main/java/io/netty/microbench/concurrent/FastThreadLocalFastPathBenchmark.java
?
備注:?在我本地進(jìn)行測試,FastThreadLocal的吞吐量是jdkThreadLocal的3倍左右。機(jī)器不一樣,可能效果也不一樣,大家可以自己試試,反正就是快了不少。
關(guān)于ThreadLocal,之前的這篇:手撕面試題ThreadLocal!!!已經(jīng)詳細(xì)介紹了。
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測試結(jié)果:是ThreadLocal的吞吐量的3倍左右。
?
?
?
FastThreadLocalSlowPathBenchmark測試結(jié)果:比ThreadLocal的吞吐量還低。
?
?
?
測試結(jié)論:使用FastThreadLocalThread線程操作FastThreadLocal才會快,如果是普通線程還更慢!
?
注釋里面給出了三點:
- FastThreadLocal操作元素的時候,使用常量下標(biāo)在數(shù)組中進(jìn)行定位元素來替代ThreadLocal通過哈希和哈希表,這個改動特別在頻繁使用的時候,效果更加顯著!
- 想要利用上面的特征,線程必須是FastThreadLocalThread或者其子類,默認(rèn)DefaultThreadFactory都是使用FastThreadLocalThread的
- 只用在FastThreadLocalThread或者子類的線程使用FastThreadLocal才會更快,因為FastThreadLocalThread 定義了屬性threadLocalMap類型是InternalThreadLocalMap。如果普通線程會借助ThreadLocal。
我們看看NioEventLoopGroup細(xì)節(jié):
?
?
?
?
?
?
?
看到這里,和剛剛我們看到的注釋內(nèi)容一致的,是使用FastThreadLocalThread的。
netty里面使用FastThreadLocal的舉例常用的:
池化內(nèi)存分配:
?
?
?
會使用到Recycler
?
?
?
而Recycler也使用了FastThreadLocal
?
?
?
我們再看看看測試類:
?
?
?
備注:我們會發(fā)現(xiàn)FastThreadLocalFastPathBenchmark里面的線程是FastThreadLocal。
?
?
?
備注:我們會發(fā)現(xiàn)FastThreadLocalSlowPathBenchmark里面的線程不是FastThreadLocal。
FastThreadLocal只有被的線程是FastThreadLocalThread或者其子類使用的時候才會更快,吞吐量我這邊測試的效果大概3倍左右,但是如果是普通線程操作FastThreadLocal其吞吐量比ThreadLocal還差!
FastThreadLocal利用字節(jié)填充來解決偽共享問題
關(guān)于CPU 緩存 內(nèi)容來源于美團(tuán):https://tech.meituan.com/2016/11/18/disruptor.html
下圖是計算的基本結(jié)構(gòu)。L1、L2、L3分別表示一級緩存、二級緩存、三級緩存,越靠近CPU的緩存,速度越快,容量也越小。所以L1緩存很小但很快,并且緊靠著在使用它的CPU內(nèi)核;L2大一些,也慢一些,并且仍然只能被一個單獨的CPU核使用;L3更大、更慢,并且被單個插槽上的所有CPU核共享;最后是主存,由全部插槽上的所有CPU核共享。
?
?
?
當(dāng)CPU執(zhí)行運算的時候,它先去L1查找所需的數(shù)據(jù)、再去L2、然后是L3,如果最后這些緩存中都沒有,所需的數(shù)據(jù)就要去主內(nèi)存拿。走得越遠(yuǎn),運算耗費的時間就越長。所以如果你在做一些很頻繁的事,你要盡量確保數(shù)據(jù)在L1緩存中。
另外,線程之間共享一份數(shù)據(jù)的時候,需要一個線程把數(shù)據(jù)寫回主存,而另一個線程訪問主存中相應(yīng)的數(shù)據(jù)。
下面是從CPU訪問不同層級數(shù)據(jù)的時間概念:
?
?
?
可見CPU讀取主存中的數(shù)據(jù)會比從L1中讀取慢了近2個數(shù)量級。
緩存行
Cache是由很多個cache line組成的。每個cache line通常是64字節(jié),并且它有效地引用主內(nèi)存中的一塊兒地址。一個Java的long類型變量是8字節(jié),因此在一個緩存行中可以存8個long類型的變量。
CPU每次從主存中拉取數(shù)據(jù)時,會把相鄰的數(shù)據(jù)也存入同一個cache line。
在訪問一個long數(shù)組的時候,如果數(shù)組中的一個值被加載到緩存中,它會自動加載另外7個。因此你能非常快的遍歷這個數(shù)組。事實上,你可以非常快速的遍歷在連續(xù)內(nèi)存塊中分配的任意數(shù)據(jù)結(jié)構(gòu)。
偽共享
由于多個線程同時操作同一緩存行的不同變量,但是這些變量之間卻沒有啥關(guān)聯(lián),但是每次修改,都會導(dǎo)致緩存的數(shù)據(jù)變成無效,從而明明沒有任何修改的內(nèi)容,還是需要去主存中讀(CPU讀取主存中的數(shù)據(jù)會比從L1中讀取慢了近2個數(shù)量級)但是其實這塊內(nèi)容并沒有任何變化,由于緩存的最小單位是一個緩存行,這就是偽共享。
如果讓多線程頻繁操作的并且沒有關(guān)系的變量在不同的緩存行中,那么就不會因為緩存行的問題導(dǎo)致沒有關(guān)系的變量的修改去影響另外沒有修改的變量去讀主存了(那么從L1中取是從主存取快2個數(shù)量級的)那么性能就會好很多很多。
有偽共享 和沒有的情況的測試效果
代碼路徑:https://github.com/jiangxinlingdu/nettydemo
nettydemo
?
?
?
?
?
利用字節(jié)填充來解決偽共享,從而速度快了3倍左右。
FastThreadLocal使用字節(jié)填充解決偽共享
之前介紹ThreadLocal的時候,說過ThreadLocal是用在多線程場景下,那么FastThreadLocal也是用在多線程場景,大家可以看下這篇:手撕面試題ThreadLocal!!!,所以FastThreadLocal需要解決偽共享問題,FastThreadLocal使用字節(jié)填充解決偽共享。
?
?
?
?
?
這個是我自己手算的,通過手算太麻煩,推薦一個工具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
nettydemo
?
?
?
通過這個工具算起來就很容易了,如果以后有類似的需要看的,不用手一個一個算了。
FastThreadLocal被FastThreadLocalThread進(jìn)行讀寫的時候也可能利用到緩存行
?
?
?
并且由于當(dāng)線程是FastThreadLocalThread的時候操作FastThreadLocal是通過indexedVariables數(shù)組進(jìn)行存儲數(shù)據(jù)的的,每個FastThreadLocal有一個常量下標(biāo),通過下標(biāo)直接定位數(shù)組進(jìn)行讀寫操作,當(dāng)有很多FastThreadLocal的時候,也可以利用緩存行,比如一次indexedVariables數(shù)組第3個位置數(shù)據(jù),由于緩存的最小單位是緩存行,順便把后面的4、5、6等也緩存了,下次剛剛好另外FastThreadLocal下標(biāo)就是5的時候,進(jìn)行讀取的時候就直接走緩存了,比走主存可能快2個數(shù)量級。
?
?
?
一點疑惑
問題:為什么這里填充了9個long值呢???
我提了一個issue:https://github.com/netty/netty/issues/9284
?
?
?
雖然也有人回答,但是感覺不是自己想要的,說服不了自己!!!
?
?
?
FastThreadLocal比ThreadLocal快,并不是空間換時間
?
?
?
?
?
現(xiàn)在清理已經(jīng)去掉,本文下面會介紹,所以FastThreadLocal比ThreadLocal快,并不是空間換時間,FastThreadLocal并沒有浪費空間!!!
FastThreadLocal不在使用ObjectCleaner處理泄漏,必要的時候建議重寫onRemoval方法
最新的netty版本中已經(jīng)不在使用ObjectCleaner處理泄漏:
https://github.com/netty/netty/commit/9b1a59df383559bc568b891d73c7cb040019aca6#diff-e0eb4e9a6ea15564e4ddd076c55978de
https://github.com/netty/netty/commit/5b1fe611a637c362a60b391079fff73b1a4ef912#diff-e0eb4e9a6ea15564e4ddd076c55978de
?
?
?
?
?
?
?
去掉原因:
https://github.com/netty/netty/issues/8017
?
?
?
我們看看FastThreadLocal的onRemoval
?
?
?
如果使用的是FastThreadLocalThread能保證調(diào)用的,重寫onRemoval做一些收尾狀態(tài)修改等等
?
?
?
FastThreadLocal為什么快?
FastThreadLocal操作元素的時候,使用常量下標(biāo)在數(shù)組中進(jìn)行定位元素來替代ThreadLocal通過哈希和哈希表,這個改動特別在頻繁使用的時候,效果更加顯著!計算該ThreadLocal需要存儲的位置是通過hash算法確定位置:
int i = key.threadLocalHashCode & (len-1);而FastThreadLocal就是一個常量下標(biāo)index,這個如果執(zhí)行次數(shù)很多也是有影響的。
并且FastThreadLocal利用緩存行的特性,FastThreadLocal是通過indexedVariables數(shù)組進(jìn)行存儲數(shù)據(jù)的,如果有多個FastThreadLocal的時候,也可以利用緩存行,比如一次indexedVariables數(shù)組第3個位置數(shù)據(jù),由于緩存的最小單位是緩存行,順便把后面的4、5、6等也緩存了,下次剛剛好改線程需要讀取另外的FastThreadLocal,這個FastThreadLocal的下標(biāo)就是5的時候,進(jìn)行讀取的時候就直接走緩存了,比走主存可能快2個數(shù)量級而ThreadLocal通過hash是分散的。
轉(zhuǎn)載于:https://www.cnblogs.com/CQqf2019/p/11155560.html
總結(jié)
以上是生活随笔為你收集整理的FastThreadLocal吞吐量居然是ThreadLocal的3倍的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 当统计信息不准确时,CBO可能产生错误的
- 下一篇: win10+Linux双系统安装及一些配