Netty FastThreadLocal 實踐

FunTester發表於2024-06-05

在效能測試當中,經常會遇到實現執行緒安全的場景。使用 ThreadLocal 是一個非常簡單且使用的解決方案。ThreadLocal 用於儲存每個執行緒獨立的變數,避免執行緒間共享資料帶來的同步問題。然而,在高併發場景下,ThreadLocal 的效能可能會受到影響,因為它依賴於雜湊表進行變數存取,存在一定的開銷。而且 ThreadLocal 也有記憶體洩露的風險,如果對於一個效能測試服務來講,ThreadLocal 的風險是顯而易見的。

最近在學習大佬的文章中發現還有一種解決方案就是 FastThreadLocal 。為了最佳化 ThreadLocal 這些效能瓶頸,Netty 引入了 FastThreadLocal。聽名字就知道比 ThreadLocal 更快。

FastThreadLocal 透過內部使用陣列代替雜湊表,從而加速變數的存取操作。它最佳化了記憶體管理,特別是減少了垃圾回收帶來的開銷,這在高效能網路應用中尤為重要。對於需要處理大量併發請求的系統,如 Netty 框架下的網路伺服器,FastThreadLocal 提供了更高效的執行緒本地儲存解決方案,顯著提升了整體效能。

FastThreadLocal VS ThreadLocal 理論對比

下面是一些兩者的對比資訊。方便大家瞭解 FastThreadLocalThreadLocal 差異和方案原理不同。

FastThreadLocalThreadLocal 都是用於執行緒本地儲存(Thread Local Storage,TLS)的 Java 工具類,但它們有一些關鍵的區別。ThreadLocal 是 Java 標準庫的一部分,而 FastThreadLocal 是 Netty 專案的一部分,專門用於最佳化效能。以下是它們的詳細對比:

基本概念

  • ThreadLocal: Java 標準庫中的一個類,每個執行緒都擁有一個獨立的變數副本,這些副本互相獨立,不會干擾其他執行緒的變數副本。
  • FastThreadLocal: Netty 提供的一個最佳化版的執行緒本地儲存,旨在提供更高效的效能和更少的記憶體開銷。

效能對比

  • ThreadLocal: 實現相對簡單,但在高併發場景下效能可能不夠理想。它的內部實現依賴於每個執行緒的 Thread 物件中的一個 ThreadLocalMap,並且需要透過雜湊查詢來訪問變數。
  • FastThreadLocal: 透過在內部採用陣列而非雜湊表來儲存變數,從而提高訪問速度。此外,它對垃圾回收也進行了最佳化,減少了記憶體開銷和 GC 停頓時間。

記憶體管理

  • ThreadLocal: 可能會導致記憶體洩漏,特別是在使用執行緒池時。如果執行緒池中的執行緒未能及時清理 ThreadLocal 變數,則可能導致這些變數無法被垃圾回收。
  • FastThreadLocal: 透過增強的記憶體管理策略減少記憶體洩漏風險。線上程池中使用時,FastThreadLocal 通常更安全,因為它可以更好地管理和清理執行緒本地變數。

使用場景

  • ThreadLocal: 適合於一般的多執行緒環境下儲存執行緒私有的變數,且對效能要求不高的場景。
  • FastThreadLocal: 適用於對效能要求高、需要處理大量併發請求的場景,特別是 Netty 等高效能網路框架中。

實踐環節

ThreadLocal 實踐

ThreadLocal 相對比較熟悉,例子也信手拈來,這裡特意多加了一個原子類,用來標記每個執行緒獲取的都是不一樣的值。

import com.funtester.frame.SourceCode  

import java.util.concurrent.atomic.AtomicInteger  

class ThreadLocalTest extends SourceCode {  

    static void main(String[] args) {  

        AtomicInteger index = new AtomicInteger(0)//執行緒安全的原子操作  

        ThreadLocal<String> threadLocal = new ThreadLocal<String>() {//執行緒區域性變數  
            @Override  
            protected String initialValue() {  
                return "Hello FunTester  " + index.getAndIncrement();//每個執行緒都會有一個獨立的副本  
            }  
        };  
        4.times {// 4次  
            fun {// 4個執行緒  
                println(threadLocal.get())//每個執行緒都會有一個獨立的副本  
            }  
        }  
    }  
}

使用了 ThreadLocal 和原子操作。讓我們逐步解析一下:

  1. AtomicInteger index = new AtomicInteger(0) 建立了一個執行緒安全的原子整數,初始值為 0。
  2. ThreadLocal<String> threadLocal = new ThreadLocal<String>() { ... } 建立了一個執行緒本地變數,用於為每個執行緒儲存一個獨立的字串副本。
  3. protected String initialValue() { ... } 重寫了 ThreadLocal 的 initialValue() 方法,用於線上程第一次訪問執行緒本地變數時設定初始值。在這裡,初始值是"Hello FunTester "加上一個原子遞增的整數。
  4. 4.times { fun { ... } } 建立了 4 個執行緒,每個執行緒執行匿名函式fun
  5. println(threadLocal.get()) 在每個執行緒中,列印當前執行緒的 ThreadLocal 值。

當執行這段程式碼時,它會輸出 4 行,每行顯示一個"Hello FunTester "加上一個不同的數字,因為每個執行緒都有自己獨立的 ThreadLocal 副本。

這個示例展示瞭如何使用 ThreadLocal 為每個執行緒建立獨立的變數副本,同時使用原子操作來確保執行緒安全。這種技術在需要執行緒隔離和避免共享變數時非常有用。

控制檯列印:

Hello FunTester  1
Hello FunTester  2
Hello FunTester  3
Hello FunTester  0
15:51:42:215 Thread-0 uptime:3 s
15:51:42:233 Thread-1 finished: 4 task

FastThreadLocal 示例

首先我們需要引入 Netty 依賴包,這裡就不展示了。FastThreadLocal 用法跟 FastThreadLocal 高度一致的,下面是展示程式碼。

import com.funtester.frame.SourceCode  
import io.grpc.netty.shaded.io.netty.util.concurrent.FastThreadLocal  

import java.util.concurrent.atomic.AtomicInteger  

class FastThreadLocalTest extends SourceCode {  

    static void main(String[] args) {  
        FastThreadLocal<String> fastThreadLocal = new FastThreadLocal<String>() {// 執行緒區域性變數  

            AtomicInteger index = new AtomicInteger(0)// 執行緒安全的原子操作  

            @Override  
            protected String initialValue() throws Exception {  
                return "Hello" + index.getAndIncrement();// 每個執行緒都會有一個獨立的副本  
            }  
        };  
        4.times {// 4次  
            fun {// 4個執行緒  
                println(fastThreadLocal.get())// 每個執行緒都會有一個獨立的副本  
            }  
        }    }  
}

這段程式碼演示瞭如何使用 FastThreadLocal 類來實現執行緒區域性變數。FastThreadLocal 是阿里巴巴開源的一個高效能執行緒區域性變數工具類,相比於 JDK 原生的 ThreadLocal 類,它能夠提供更好的效能。

讓我們逐步分析這段程式碼:

  1. FastThreadLocal<String> fastThreadLocal = new FastThreadLocal<String>() {...} 建立了一個 FastThreadLocal 例項,用於為每個執行緒儲存一個獨立的字串副本。
  2. AtomicInteger index = new AtomicInteger(0); 在匿名內部類中建立了一個執行緒安全的原子整數,初始值為 0。
  3. protected String initialValue() throws Exception {...} 重寫了 FastThreadLocal 的 initialValue() 方法,用於線上程第一次訪問執行緒區域性變數時設定初始值。在這裡,初始值是字串 "Hello" 加上一個原子遞增的整數。
  4. 4.times { fun { ... } } 建立了 4 個執行緒,每個執行緒執行匿名函式 fun
  5. println(fastThreadLocal.get()) 在每個執行緒中,列印當前執行緒的 FastThreadLocal 值。

當你執行這段程式碼時,它會輸出 4 行,每行顯示一個 "Hello" 加上一個不同的數字,因為每個執行緒都有自己獨立的 FastThreadLocal 副本。

與 ThreadLocal 類類似,FastThreadLocal 也為每個執行緒提供了一個獨立的變數副本,但它的實現方式更加高效,尤其在高併發場景下,能夠顯著提高效能。

值得注意的是,雖然 FastThreadLocal 提供了更好的效能,但它缺少了一些 ThreadLocal 的高階特性,如覆寫 setremove 等方法。因此,在選擇使用 FastThreadLocal 還是 ThreadLocal 時,需要權衡效能和功能需求。

JMH 效能測試

讓我們簡單寫一個 JMH 微基準測試一下,測試結果僅供參考,如果各位要選擇的話,建議使用更加符合實際使用場景的 Case。


import io.grpc.netty.shaded.io.netty.util.concurrent.FastThreadLocal;  
import org.openjdk.jmh.annotations.*;  
import org.openjdk.jmh.results.format.ResultFormatType;  
import org.openjdk.jmh.runner.Runner;  
import org.openjdk.jmh.runner.RunnerException;  
import org.openjdk.jmh.runner.options.Options;  
import org.openjdk.jmh.runner.options.OptionsBuilder;  

import java.util.concurrent.TimeUnit;  

@BenchmarkMode(Mode.Throughput)  
@State(value = Scope.Thread)//預設為Scope.Thread,含義是每個執行緒都會有一個例項  
@OutputTimeUnit(TimeUnit.MICROSECONDS)  
public class FunTester {  

    ThreadLocal<String> threadLocal = ThreadLocal.withInitial(() -> "hello FunTester");  

    FastThreadLocal<String> fastThreadLocal = new FastThreadLocal<String>() {  
        @Override  
        protected String initialValue() throws Exception {  
            return "hello FunTester";  
        }  
    };  

    @Benchmark  
    public void threadLocal() {  
        threadLocal.get();  
    }  

    @Benchmark  
    public void fastLocal() {  
        fastThreadLocal.get();  
    }  

    public static void main(String[] args) throws RunnerException {  
        Options options = new OptionsBuilder()  
                .include(FunTester.class.getSimpleName())//測試類名  
                .result("long/result.json")//測試結果輸出到result.json檔案  
                .resultFormat(ResultFormatType.JSON)//輸出格式  
                .forks(1)//fork表示每個測試會fork出幾個程序,也就是說每個測試會跑幾次  
                .threads(40)//測試執行緒數  
                .warmupIterations(2)//預熱次數  
                .warmupBatchSize(2)//預熱批次大小  
                .measurementIterations(1)//測試迭代次數  
                .measurementBatchSize(1)//測試批次大小  
                .build();  
        new Runner(options).run();  
    }  


}

微基準測試結果:

Benchmark               Mode  Cnt     Score   Error   Units
FunTester.fastLocal    thrpt       4252.047          ops/us
FunTester.threadLocal  thrpt       7128.178          ops/us
  • 2021 年原創合集
  • 2022 年原創合集
  • 2023 年原創合集
  • 服務端功能測試
  • 效能測試專題
  • Java、Groovy、Go、Python
  • 單元&白盒&工具合集
  • 測試方案&BUG&爬蟲&UI 自動化
  • 測試理論雞湯
  • 社群風采&影片合集
如果覺得我的文章對您有用,請隨意打賞。您的支援將鼓勵我繼續創作!
打賞支援
暫無回覆。

相關文章