驚:FastThreadLocal吞吐量居然是ThreadLocal的3倍!!!


說明

接着上次手撕面試題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核共享。

img

當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

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

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/commit/9b1a59df383559bc568b891d73c7cb040019aca6#diff-e0eb4e9a6ea15564e4ddd076c55978de

https://github.com/netty/netty/commit/5b1fe611a637c362a60b391079fff73b1a4ef912#diff-e0eb4e9a6ea15564e4ddd076c55978de

去掉原因:

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是分散的。


**如果讀完覺得有收獲的話,歡迎點贊、關注、加公眾號 [匠心零度] ,查閱更多精彩歷史!!! **


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM