写点什么

Netty FastThreadLocal 实践

作者:FunTester
  • 2024-06-05
    河北
  • 本文字数:4199 字

    阅读完需:约 14 分钟

在性能测试当中,经常会遇到实现线程安全的场景。使用 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  1Hello FunTester  2Hello FunTester  3Hello FunTester  015:51:42:215 Thread-0 uptime:3 s15: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   UnitsFunTester.fastLocal    thrpt       4252.047          ops/usFunTester.threadLocal  thrpt       7128.178          ops/us
复制代码

发布于: 刚刚阅读数: 6
用户头像

FunTester

关注

公众号:FunTester,800篇原创,欢迎关注 2020-10-20 加入

Fun·BUG挖掘机·性能征服者·头顶锅盖·Tester

评论

发布
暂无评论
Netty FastThreadLocal实践_FunTester_InfoQ写作社区