Netty FastThreadLocal实践
在性能测试当中,经常会遇到实现线程安全的场景。使用 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