在效能測試當中,經常會遇到實現執行緒安全的場景。使用 ThreadLocal
是一個非常簡單且使用的解決方案。ThreadLocal 用於儲存每個執行緒獨立的變數,避免執行緒間共享資料帶來的同步問題。然而,在高併發場景下,ThreadLocal
的效能可能會受到影響,因為它依賴於雜湊表進行變數存取,存在一定的開銷。而且 ThreadLocal
也有記憶體洩露的風險,如果對於一個效能測試服務來講,ThreadLocal
的風險是顯而易見的。
最近在學習大佬的文章中發現還有一種解決方案就是 FastThreadLocal
。為了最佳化 ThreadLocal
這些效能瓶頸,Netty 引入了 FastThreadLocal
。聽名字就知道比 ThreadLocal
更快。
FastThreadLocal
透過內部使用陣列代替雜湊表,從而加速變數的存取操作。它最佳化了記憶體管理,特別是減少了垃圾回收帶來的開銷,這在高效能網路應用中尤為重要。對於需要處理大量併發請求的系統,如 Netty 框架下的網路伺服器,FastThreadLocal
提供了更高效的執行緒本地儲存解決方案,顯著提升了整體效能。
FastThreadLocal VS ThreadLocal 理論對比
下面是一些兩者的對比資訊。方便大家瞭解 FastThreadLocal
與 ThreadLocal
差異和方案原理不同。
FastThreadLocal
和 ThreadLocal
都是用於執行緒本地儲存(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 和原子操作。讓我們逐步解析一下:
-
AtomicInteger index = new AtomicInteger(0)
建立了一個執行緒安全的原子整數,初始值為 0。 -
ThreadLocal<String> threadLocal = new ThreadLocal<String>() { ... }
建立了一個執行緒本地變數,用於為每個執行緒儲存一個獨立的字串副本。 -
protected String initialValue() { ... }
重寫了 ThreadLocal 的 initialValue() 方法,用於線上程第一次訪問執行緒本地變數時設定初始值。在這裡,初始值是"Hello FunTester "加上一個原子遞增的整數。 -
4.times { fun { ... } }
建立了 4 個執行緒,每個執行緒執行匿名函式fun
。 -
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 類,它能夠提供更好的效能。
讓我們逐步分析這段程式碼:
-
FastThreadLocal<String> fastThreadLocal = new FastThreadLocal<String>() {...}
建立了一個 FastThreadLocal 例項,用於為每個執行緒儲存一個獨立的字串副本。 -
AtomicInteger index = new AtomicInteger(0);
在匿名內部類中建立了一個執行緒安全的原子整數,初始值為 0。 -
protected String initialValue() throws Exception {...}
重寫了 FastThreadLocal 的 initialValue() 方法,用於線上程第一次訪問執行緒區域性變數時設定初始值。在這裡,初始值是字串 "Hello" 加上一個原子遞增的整數。 -
4.times { fun { ... } }
建立了 4 個執行緒,每個執行緒執行匿名函式fun
。 -
println(fastThreadLocal.get())
在每個執行緒中,列印當前執行緒的 FastThreadLocal 值。
當你執行這段程式碼時,它會輸出 4 行,每行顯示一個 "Hello" 加上一個不同的數字,因為每個執行緒都有自己獨立的 FastThreadLocal 副本。
與 ThreadLocal 類類似,FastThreadLocal 也為每個執行緒提供了一個獨立的變數副本,但它的實現方式更加高效,尤其在高併發場景下,能夠顯著提高效能。
值得注意的是,雖然 FastThreadLocal 提供了更好的效能,但它缺少了一些 ThreadLocal 的高階特性,如覆寫 set
、remove
等方法。因此,在選擇使用 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 自動化
- 測試理論雞湯
- 社群風采&影片合集