【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,则其绑定的变量值存储在 indexedVariables[3]

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 实例会被分配一个唯一的整数索引(通过静态计数器生成),变量的存储和读取直接通过该索引操作数组(array[index])。

      这种「索引直接访问」方式完全避免了哈希计算和哈希冲突的处理,访问效率接近数组的 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);
        }
    }
}

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


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

四、参考内容

  1. 豆包
  2. 《Netty4 核心原理》
  3. InheritableThreadLocal 详解
相关推荐
后端小张5 天前
【JAVA 进阶】深入探秘Netty之Reactor模型:从理论到实战
java·开发语言·网络·spring boot·spring·reactor·netty
❀͜͡傀儡师8 天前
springboot集成mqtt服务,自主下发
java·spring boot·后端·mqtt·netty
onAcorner15 天前
Netty/Redis网络模型——IO多路复用原理(操作系统)
netty·nio
tanxinji15 天前
Netty编写Echo服务器
java·netty
fanly1116 天前
在抖音直播推广开源作品的可行性?
微服务·netty·.net core·microservice
9527出列19 天前
Netty源码分析(终)--关于WriteAndFlush
netty·源码阅读
C2H5OH66620 天前
Netty详解-02
java·websocket·网络协议·tcp/ip·tomcat·netty·nio
9527出列24 天前
Netty源码分析(六)--关于ChannelPipeline
netty·源码阅读
Luo_xguan1 个月前
一、Netty-高并发IO底层原理(5种主要的IO模型)
java·服务器·netty·nio