【Netty4核心原理⑯】【Netty高性能调优工具类解析】

文章目录

  • 一、前言
  • [二、多线程共享 FastThreadLocal](#二、多线程共享 FastThreadLocal)
    • [1. FastThreadLocal 的使用](#1. FastThreadLocal 的使用)
    • [2. FastThreadLocal 原理简述](#2. FastThreadLocal 原理简述)
    • [3. FastThreadLocal 相较于 ThreadLocal 的优化](#3. FastThreadLocal 相较于 ThreadLocal 的优化)
    • [4. InheritableThreadLocal 和 TransmittableThreadLocal](#4. InheritableThreadLocal 和 TransmittableThreadLocal)
  • [三、Recycler 对象回收](#三、Recycler 对象回收)
  • 四、参考内容

一、前言

本系列虽说本意是作为 《Netty4 核心原理》一书的读书笔记,但在实际阅读记录过程中加入了大量个人阅读的理解和内容,因此对书中内容存在大量删改。

本篇涉及内容 :第十四章 Netty高性能调优工具类解析

书中 第十三章 基于 Netty 手写消息推送系统 内容本系列不记录。


本系列内容基于 Netty 4.1.73.Final 版本,如下:

xml 复制代码
 <dependency>
    <groupId>io.netty</groupId>
     <artifactId>netty-all</artifactId>
     <version>4.1.73.Final</version>
 </dependency>

系列文章目录:

【Netty4核心原理】【全系列文章目录】

二、多线程共享 FastThreadLocal

我们在 之前的文章中曾简单介绍过 FastThreadLocal,他类似于 JDK 的 ThreadLocal,也是用于多线程条件下,保证统一线程的对象共享,只是 Netty 中定义的 FastThreadLocal 性能要高于 JDK 的 ThreadLocal。

1. FastThreadLocal 的使用

FastThreadLocal 的使用与 ThreadLocal 完全相同,如下:

java 复制代码
    public static void main(String[] args) {
        FastThreadLocal<String> threadLocal = new FastThreadLocal<>() {
            @Override
            protected String initialValue() {
                return "init";
            }
        };

        ExecutorService executorService = Executors.newFixedThreadPool(10);
        executorService.execute(() -> {
            try {
                log.info("start {}", threadLocal.get());
                threadLocal.set(String.valueOf(Thread.currentThread().getId()));
                log.info("end {}", threadLocal.get());
            } finally {
                threadLocal.remove();
            }
        });
      

        ThreadUtil.sleep(1000);
    }

2. FastThreadLocal 原理简述

在分析 FastThreadLocal 之前我们需要先了解 ThreadLocal 的基本原理,这部分内容在 ThreadLocal使用及源码分析 中有过分析(已经过去这么久了,岁月如梭啊 ),因此本文不再说明。

在介绍 FastThreadLocal 之间我们需要先介绍下 InternalThreadLocalMap,

2.1 InternalThreadLocalMap

InternalThreadLocalMap 是 Netty 内部用于优化线程本地变量存储的核心类,是 FastThreadLocal 机制的底层支撑。它通过数组而非哈希表存储线程本地变量,实现了更高效的访问性能,

InternalThreadLocalMap 替代了 JDK 原生的 ThreadLocalMap,为 FastThreadLocal 提供存储容器,主要解决以下问题:

  • 避免 JDK ThreadLocal 中哈希表的哈希计算和冲突处理开销。
  • 提供 O (1) 时间复杂度的变量访问(通过数组下标)。
  • 支持批量清理线程本地变量,减少内存泄漏风险。

我们这里简单来看 InternalThreadLocalMap 下面几点:

2.1.1 InternalThreadLocalMap 的构造

InternalThreadLocalMap 的构造方法如下,

java 复制代码
    private InternalThreadLocalMap() {
    	// 初始化存储线程本地变量的数组,使用默认初始容量
        indexedVariables = newIndexedVariableTable();
    }

	private static Object[] newIndexedVariableTable() {
	    // 创建指定初始容量的数组(INDEXED_VARIABLE_TABLE_INITIAL_SIZE 默认为32,容量不足时自动扩容)
	    Object[] array = new Object[INDEXED_VARIABLE_TABLE_INITIAL_SIZE];
	    // 将数组所有元素初始化为UNSET(一个特殊的标记对象)
	    // UNSET用于区分"未初始化"和"值为null"两种状态
	    Arrays.fill(array, UNSET);
	    return array;
	}

通过上面的构造函数我们可以知道下面几点:

  1. InternalThreadLocalMap 内部使用 Object\[\] 数组存储线程本地变量。

    InternalThreadLocalMap 与 FastThreadLocal 是一对多,而 FastThreadLocal 与 Thread 是一对一,所以 InternalThreadLocalMap 与 Thread 是一对多,每个FastThreadLocal 在初始化的时候会的构造函数中会 InternalThreadLocalMap#nextVariableIndex 为自身分配一个唯一的 index,这个 index 代表当前 FastThreadLocal 的变量存储在 InternalThreadLocalMap#indexedVariables 数组的第几个元素中。

  2. InternalThreadLocalMap 会为 Object\[\] 每个元素分配一个 UNSET 默认值,UNSET 值默认为未初始化,null 则表示初始化的值为 null。

2.1.1 InternalThreadLocalMap#get

InternalThreadLocalMap#get 用于从 ThreadLocal 中获取变量,具体实现如下:

java 复制代码
public static InternalThreadLocalMap get() {
    // 获取当前执行线程的实例
    Thread thread = Thread.currentThread();
    
    // 检查当前线程是否是Netty自定义的FastThreadLocalThread
    if (thread instanceof FastThreadLocalThread) {
        // 若是Netty自定义线程,走高效获取路径
        return fastGet((FastThreadLocalThread) thread);
    } else {
        // 若是普通线程,走兼容获取路径(性能略低)
        return slowGet();
    }
}

/**
 * 针对FastThreadLocalThread的高效获取方法
 * 直接访问线程内部持有的InternalThreadLocalMap引用,避免中间层开销
 *
 * @param thread 类型为FastThreadLocalThread的当前线程
 * @return 当前线程关联的InternalThreadLocalMap实例(非null)
 */
private static InternalThreadLocalMap fastGet(FastThreadLocalThread thread) {
    // 直接从线程实例中获取已关联的InternalThreadLocalMap
    InternalThreadLocalMap threadLocalMap = thread.threadLocalMap();
    
    // 如果线程尚未关联InternalThreadLocalMap,创建并绑定
    if (threadLocalMap == null) {
        // 1. 创建新的InternalThreadLocalMap实例
        // 2. 通过setThreadLocalMap方法绑定到当前线程
        // 3. 赋值给threadLocalMap变量(一行代码完成三步操作)
        thread.setThreadLocalMap(threadLocalMap = new InternalThreadLocalMap());
    }
    
    return threadLocalMap;
}

/**
 * 针对普通线程的兼容获取方法
 * 通过JDK原生ThreadLocal间接获取,性能略低于fastGet
 *
 * @return 当前线程关联的InternalThreadLocalMap实例(非null)
 */
private static InternalThreadLocalMap slowGet() {
    // slowThreadLocalMap是一个JDK原生ThreadLocal实例,用于关联普通线程和InternalThreadLocalMap
    // 这里通过它获取当前线程已关联的InternalThreadLocalMap
    InternalThreadLocalMap ret = slowThreadLocalMap.get();
    
    // 如果普通线程尚未关联InternalThreadLocalMap,创建并绑定
    if (ret == null) {
        // 创建新的InternalThreadLocalMap实例
        ret = new InternalThreadLocalMap();
        // 通过JDK ThreadLocal绑定到当前线程
        slowThreadLocalMap.set(ret);
    }
    
    return ret;
}

这里我们可以知道:

  • InternalThreadLocalMap#fastGet 方法针对于 FastThreadLocalThread 场景,这是 Netty 做了优化的处理,因此更快。 fastGet 方法更快的原因如下:
    • 直接引用访问:FastThreadLocalThread 内部直接持有 InternalThreadLocalMap 成员变量,通过 threadLocalMap() 方法直接获取,无需任何哈希计算或间接查找,是真正的 O (1) 操作。
    • 延迟初始化:仅在首次调用时创建 InternalThreadLocalMap 并绑定到线程,避免资源浪费。
  • InternalThreadLocalMap#slowGet 则使用原生 JDK 的方式,相较于 Netty 的优化版本会更慢一些。

上面分析了 InternalThreadLocalMap 的部分内容,下面我们来看看 FastThreadLocal 的部分方法。

2.2 FastThreadLocal 构造函数

FastThreadLocal 构造函数如下,其中会调用 InternalThreadLocalMap.nextVariableIndex() 来获取自己的下标

java 复制代码
    public FastThreadLocal() {
        index = InternalThreadLocalMap.nextVariableIndex();
    }

其中 InternalThreadLocalMap#nextVariableIndex 的实现如下:

java 复制代码
    private static final AtomicInteger nextIndex = new AtomicInteger();

    public static int nextVariableIndex() {
    	// 原子自增
        int index = nextIndex.getAndIncrement();
        // 判断是否超过最大限制
        if (index >= ARRAY_LIST_CAPACITY_MAX_SIZE || index < 0) {
            nextIndex.set(ARRAY_LIST_CAPACITY_MAX_SIZE);
            throw new IllegalStateException("too many thread-local indexed variables");
        }
        return index;
    }

FastThreadLocal#index 代表当前线程的变量存储在 InternalThreadLocalMap#indexedVariables 数组的第几个元素中。

如 FastThreadLocal 实例 A 的 index 为 3,则其绑定的变量值存储在 indexedVariables3

2.3 FastThreadLocal#get

FastThreadLocal#get 是 Netty 对 JDK 原生 ThreadLocal 的优化实现,用于快速获取当前线程关联的线程本地变量。FastThreadLocal#get 实现如下:

java 复制代码
/**
 * 获取当前线程绑定的线程本地变量值
 * 如果变量未初始化,则通过initialize()方法完成初始化并绑定
 * 
 * @return 当前线程关联的变量值,非null(除非initialValue()返回null)
 */
public final V get() {
    // 1. 获取当前线程对应的InternalThreadLocalMap
    // (Netty自定义的线程本地变量存储容器,替代JDK的ThreadLocalMap)
    InternalThreadLocalMap threadLocalMap = InternalThreadLocalMap.get();
    
    // 2. 通过当前FastThreadLocal的唯一索引(index)直接访问数组中的值
    // (相比JDK的哈希表查找,数组下标访问效率更高,O(1)时间复杂度)
    Object v = threadLocalMap.indexedVariable(index);
    
    // 3. 如果值已初始化(不等于UNSET标记),直接返回
    if (v != InternalThreadLocalMap.UNSET) {
        return (V) v;
    }

    // 4. 若未初始化,调用initialize()方法完成初始化并返回结果
    return initialize(threadLocalMap);
}

下面我们按照上面注释来说明这几步:

  1. 获取存储容器:通过 InternalThreadLocalMap.get() 获取当前线程绑定的专属存储容器,该容器内部用数组存储线程本地变量。

  2. 直接索引访问:利用 index(每个 FastThreadLocal 实例的唯一标识)直接从数组中获取值,避免了 JDK ThreadLocal 的哈希计算和冲突处理,这是性能优化的关键。

  3. 状态判断:InternalThreadLocalMap.UNSET 是一个特殊标记,用于表示变量尚未初始化(区别于 null,因为用户可能需要存储 null 作为有效值)。

  4. 延迟初始化:若变量未初始化,则委托 initialize() 方法执行初始化逻辑(通常会调用 initialValue() 生成初始值,并将其绑定到当前线程的 InternalThreadLocalMap 中)。


2.3 FastThreadLocal#set

FastThreadLocal#set 用于为当前线程的 FastThreadLocal 实例设置值,是 FastThreadLocal 核心操作之一。其实现基于数组索引访问,相比 ThreadLocal 的哈希表操作,具有更高的效率。该方法具体实现如下:

java 复制代码
    public final void set(V value) {
    	// 1. 判断是否没有被设值
        if (value != InternalThreadLocalMap.UNSET) {
            InternalThreadLocalMap threadLocalMap = InternalThreadLocalMap.get();
            setKnownNotUnset(threadLocalMap, value);
        } else {
            remove();
        }
    }

    private void setKnownNotUnset(InternalThreadLocalMap threadLocalMap, V value) {
    	// 直接通过索引设置数组值,无需哈希计算
        if (threadLocalMap.setIndexedVariable(index, value)) {
        	// 若为首次设置,将当前FastThreadLocal添加到map的"已注册"集合中,便于后续清理
            addToVariablesToRemove(threadLocalMap, this);
        }
    }

3. FastThreadLocal 相较于 ThreadLocal 的优化

  1. 数据结构:数组索引访问 vs 哈希表查找

    • ThreadLocal 的实现:JDK 的 ThreadLocal 依赖线程(Thread)中的 ThreadLocalMap 存储数据。ThreadLocalMap 是一种自定义的哈希表,其 key 是 ThreadLocal 实例本身,value 是线程本地变量的值。

      当调用 get() 方法时,需要通过哈希计算确定 key 在哈希表中的位置,若发生哈希冲突,还需通过「线性探测」法查找下一个可用位置。这种哈希表的查找、扩容等操作存在一定的性能开销。

    • FastThreadLocal 的实现:FastThreadLocal 基于 Netty 自定义的 InternalThreadLocalMap,其底层使用数组存储数据。每个 FastThreadLocal 实例会被分配一个唯一的整数索引(通过静态计数器生成),变量的存储和读取直接通过该索引操作数组(arrayindex)。

      这种「索引直接访问」方式完全避免了哈希计算和哈希冲突的处理,访问效率接近数组的 O (1) 操作,远快于哈希表的查找。

  2. 线程绑定:更直接的内存访问

    • ThreadLocal:要获取线程的 ThreadLocalMap,需要通过 Thread.currentThread().threadLocals 间接访问,这涉及到对象属性的多级访问(线程实例 → ThreadLocalMap 实例)。
    • FastThreadLocal:要求线程必须是 Netty 自定义的 FastThread(或其子类),这类线程直接持有 InternalThreadLocalMap 的引用,且通过 Unsafe 类(底层直接操作内存)快速获取该 map,减少了中间环节的开销。
  3. 内存布局:连续内存的缓存友好性

    数组的内存是连续的,而哈希表(如 ThreadLocalMap)的节点内存通常是分散的。CPU 缓存对连续内存的访问效率更高(缓存命中率高),因此 FastThreadLocal 基于数组的实现更利于 CPU 缓存利用,进一步提升访问速度。

  4. 清理机制:更高效的资源释放

    • ThreadLocal 的清理(如 remove())需要遍历哈希表,找到对应 key 才能删除,操作相对繁琐。
    • FastThreadLocal 由于通过索引定位,清理时只需直接操作数组对应位置,且 InternalThreadLocalMap 提供了批量清理机制,效率更高。

FastThreadLocal 给出的灵感:并不需要一味追求 Map 结构,在一些固定场景下,直接使用数组结构效率可能更高。

4. InheritableThreadLocal 和 TransmittableThreadLocal

ThreadLocal为每个使用该变量的线程都提供一个独立的变量副本,每个线程都可以独立地改变自己的副本,而不会影响其他线程所对应的副本,但 ThreadLocal 无法跨线程传递变量,因此 JDK 提供了 InheritableThreadLocal

但因为 InheritableThreadLocal 无法解决线程复用导致的上下文丢失问题,所以在线程池场景下 可使用阿里开源的TransmittableThreadLocal。


三、Recycler 对象回收

Recycler 是 Netty 实现的一个轻量级对象回收站,很多对象使用完毕之后并没有直接交给 GC 处理,而是通过对象回收站讲对象回收,目的是为了对象重用和减少 GC 压力。

比如 ByteBuf 对象的回收,因为 ByteBuf 对象在 Netty 中会被频繁创建,并且占用比较大的内存空间,所以使用完毕后会通过对象回收站的方式进行回收,以达到资源重用的目的。


在 Netty 中 Recycler 的使用相当频繁,以下面 Demo为例:

java 复制代码
public class RecyclerDemo {
    public static void main(String[] args) {
        Recycler<MyObject> recycler = new Recycler<>() {
            protected MyObject newObject(Handle<MyObject> handle) {
                return new MyObject(handle);
            }
        };

        // 创建 myObject1 并释放
        MyObject myObject1 = recycler.get();
        myObject1.recycle();
        
        // 创建 myObject2 并释放
        MyObject myObject2 = recycler.get();
        myObject2.recycle();

        // 比较 myObject1 和 myObject2 是否是同一个对象 : true
        System.out.println(myObject1 == myObject2);


    }

    public static class MyObject {
        private final Recycler.Handle<MyObject> handle;

        public MyObject(Recycler.Handle<MyObject> handle) {
            this.handle = handle;
        }

        public void recycle() {
            handle.recycle(this);
        }
    }
}

上面的代码会创建 myObject1 和 myObject2 两个对象,由于创建 myObject2 时 myObject1 已经回收,所以会被 myObject2 复用,因此这里在比较 myObject1 == myObject2 时实际为 true。


书中有具体的分析逻辑,但是由于不想写了,所以此处省略。

四、参考内容

  1. 豆包
  2. 《Netty4 核心原理》
  3. InheritableThreadLocal 详解
相关推荐
TAKETODAY2 天前
Netty 是怎么把 HTTP 请求交给 MVC 的?一个请求的完整链路
java·netty
小鱼,2 个月前
Netty对接医疗监护仪HL7协议,使用MLLP解码器解析数据
netty·hl7
GitLqr2 个月前
Java 26 终于原生支持 HTTP/3 了:告别 Netty,直接用 QUIC
java·netty·http3
唐青枫2 个月前
Java Reactor Netty 实战详解:从响应式 HTTP 到 TCP 长连接
java·netty
皮皮林5513 个月前
Netty 性能好的原因是什么?
netty
white_ant3 个月前
5-Netty WebSocket 与 HTTP
java·网络编程·netty
ywl4708120873 个月前
【Spring AI 入门07】如何具体实现低延迟大模型推理网关
大模型·netty
ywl4708120873 个月前
第二章Netty,半包读取问题
netty·nio
ps酷教程3 个月前
WebSocketFrameEncoder&WebSocketFrameDecoder源码浅析
websocket·netty
ywl4708120873 个月前
第一章Netty,如何实现I/O多路复用的功能
netty·nio·selector