FunTester Netty FastThreadLocal 实践

FunTester · 2024年06月05日 · 1436 次阅读

在性能测试当中,经常会遇到实现线程安全的场景。使用 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
如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
暂无回复。
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册