【FastThreadLocal】FastThreadLocal的实现机制和原理

1  前言

ThreadLocal 是一个常用的工具类,它允许我们创建线程局部变量。这意味着每个线程都可以独立地改变自己的副本,而不会影响其他线程所持有的数据。然而 ThreadLocal 在高并发环境下存在一些问题:

(1)内存占用:每个 ThreadLocal 变量都会在每个线程中持有一个独立的副本,这可能导致大量的内存占用。

(2)性能开销:创建和销毁这些线程局部变量会带来额外的性能开销。

Netty 是一个追求极致高性能的组件, Netty 的 FastThreadLocal 就是为了解决这些问题而诞生的。

2  FastThreadLocal

2.1  FastThreadLocal 的认识

Netty 的 FastThreadLocal 是一个高性能的线程本地变量实现,它与 Java 标准库中的 ThreadLocal 类似,但具有更高的性能和更低的内存消耗。

FastThreadLocal 是由 Netty 框架提供的一个组件,旨在提供一种更快速、更高效的线程本地变量解决方案。

Netty 的 FastThreadLocal 使用了线程局部存储(Thread-Local Storage, TLS)的概念,但它通过一些优化手段减少了内存占用和性能开销。

(1)内存池化:FastThreadLocal 使用了一个对象池来管理线程局部变量的实例,从而避免了频繁的创建和销毁操作。
(2)索引快速访问:FastThreadLocal 使用了一个数组来存储每个线程的局部变量副本。通过 AtomicInteger 得出 FTL 索引,所以 FastThreadLocal 可以快速地访问和修改线程局部变量的值。

FastThreadLocal 的原理与ThreadLocal类似,都是通过在每个线程中维护一个线程本地变量的副本来实现的。不同之处在于,FastThreadLocal 使用了一种更加高效的数据结构来管理线程本地变量,从而提高了访问速度和减少了内存消耗。

2.2  FastThreadLocal 的使用

public class Demo {

    // FAST_THREAD_LOCAL FTL
    private static final FastThreadLocal<Integer> FAST_THREAD_LOCAL = new FastThreadLocal<>();

    public static void main(String[] args) throws InterruptedException {
        // 设置 ftl 变量
        FAST_THREAD_LOCAL.set(1);

        // 子线程获取
        Thread thread = new Thread(() -> {
            System.out.println(String.format("线程:%s,获取的value=%s", Thread.currentThread().getName(), FAST_THREAD_LOCAL.get()));
        });
        thread.start();
        thread.join();

        // 主线程获取
        System.out.println(String.format("线程:%s,获取的value=%s", Thread.currentThread().getName(), FAST_THREAD_LOCAL.get()));

        // 清除变量
        FAST_THREAD_LOCAL.remove();
    }
}

我们首先在 main 线程中设置了一个线程变量,然后开辟了一个子线程获取,获取的为空,说明 FTL 本身不具备父子传递或者线程间的传递,main 线程获取变量然后清除。

体验下来, FTL 大概跟普通的 TL 没什么区别,我们接下来就从源码看下,他俩到底区别在哪。

2.3  FastThreadLocal 源码分析

FastThreadLocal 的实现与 ThreadLocal 非常类似,Netty 为 FastThreadLocal 量身打造了 FastThreadLocalThread 和 InternalThreadLocalMap 两个重要的类。

下面我们看下这两个类是如何实现的。 FastThreadLocalThread 是对 Thread 类的一层包装,每个线程对应一个 InternalThreadLocalMap 实例。

只有 FastThreadLocal 和 FastThreadLocalThread 组合使用时,才能发挥 FastThreadLocal 的性能优势。

2.3.1  FastThreadLocalThread

首先看下 FastThreadLocalThread 的源码定义:

public class FastThreadLocalThread extends Thread {
    private InternalThreadLocalMap threadLocalMap;
    // ...
}

可以看出 FastThreadLocalThread 主要扩展了 InternalThreadLocalMap 字段,FastThreadLocalThread 主要使用 InternalThreadLocalMap 存储数据

注意, FastThreadLocalThread 不再是使用 Thread 中的ThreadLocalMap(前提是使用了FastThreadLocalThread,不使用的话他会退化到 ThreadLocal,下边会说)。

所以想知道 FastThreadLocalThread 高性能的奥秘,必须要了解InternalThreadLocalMap 的设计原理。

2.3.2  InternalThreadLocalMap

ThreadLocal 的一个重要缺点,就是 ThreadLocalMap 采用线性探测法解决 Hash冲突性能较慢,那么 InternalThreadLocalMap 又是如何优化的呢?

InternalThreadLocalMap 的内部构造,首先它继承了 UnpaddedInternalThreadLocalMap,我们合起来看看:

class UnpaddedInternalThreadLocalMap {

    // 慢的 这个意思就是当线程不是 FastThreadLocalThread 的情况下,InternalThreadLocalMap 就从这个里边取,也就是 ThreadLocal 里取,相当于退化回了 ThreadLocal
    static final ThreadLocal<InternalThreadLocalMap> slowThreadLocalMap = new ThreadLocal<InternalThreadLocalMap>();
    // FTL 计数器 默认从1开始
    static final AtomicInteger nextIndex = new AtomicInteger();
    // 存放 FTL 的线程变量 比如获取某个 FTL 的线程变量,每个 FTL 都会根据 nextIndex 得出它的 index 直接根据数组下标即可拿出线程变量
    /** Used by {@link FastThreadLocal} */
    Object[] indexedVariables;

    // Core thread-locals
    int futureListenerStackDepth;
    int localChannelReaderStackDepth;
    Map<Class<?>, Boolean> handlerSharableCache;
    IntegerHolder counterHashCode;
    ThreadLocalRandom random;
    Map<Class<?>, TypeParameterMatcher> typeParameterMatcherGetCache;
    Map<Class<?>, Map<String, TypeParameterMatcher>> typeParameterMatcherFindCache;

    // String-related thread-locals
    StringBuilder stringBuilder;
    Map<Charset, CharsetEncoder> charsetEncoderCache;
    Map<Charset, CharsetDecoder> charsetDecoderCache;

    // ArrayList-related thread-locals
    ArrayList<Object> arrayList;

    UnpaddedInternalThreadLocalMap(Object[] indexedVariables) {
        this.indexedVariables = indexedVariables;
    }
}
// InternalThreadLocalMap
public final class InternalThreadLocalMap extends UnpaddedInternalThreadLocalMap {

    private static final InternalLogger logger = InternalLoggerFactory.getInstance(InternalThreadLocalMap.class);

    private static final int DEFAULT_ARRAY_LIST_INITIAL_CAPACITY = 8;
    private static final int STRING_BUILDER_INITIAL_SIZE;
    private static final int STRING_BUILDER_MAX_SIZE;

    public static final Object UNSET = new Object();

    private BitSet cleanerFlags;

    static {
        STRING_BUILDER_INITIAL_SIZE =
                SystemPropertyUtil.getInt("io.netty.threadLocalMap.stringBuilder.initialSize", 1024);
        logger.debug("-Dio.netty.threadLocalMap.stringBuilder.initialSize: {}", STRING_BUILDER_INITIAL_SIZE);

        STRING_BUILDER_MAX_SIZE = SystemPropertyUtil.getInt("io.netty.threadLocalMap.stringBuilder.maxSize", 1024 * 4);
        logger.debug("-Dio.netty.threadLocalMap.stringBuilder.maxSize: {}", STRING_BUILDER_MAX_SIZE);
    }
    // ...
}

InternalThreadLocalMap 内部实现来看,与 ThreadLocalMap 一样都是采用数组的存储方式。但是InternalThreadLocalMap 并没有使用线性探测法来解决 Hash 冲突,而是另辟蹊径,使用数组替代map。

简单来说,而是在 FastThreadLocal 初始化 的时候,为每一个本地变量,分配一个全局唯一的索引 index ,数组索引 index 的值采用原子类 AtomicInteger 保证顺序递增.

然后在读写数据的时候通过数组下标index直接定位到 FastThreadLocal 的位置,时间复杂度为 O(1)。

和 普通的ThreadLocalMap 相比, InternalThreadLocalMap 的大致内部结构,如下:

有一批数据需要添加到数组中,分别为 value1、value2、value3、value4,对应的 FastThreadLocal 在初始化的时候生成的数组索引分别为 1、2、3、4。

当访问某个线程变量的时候,直接根据当前的 FTL 的下标直接去 InternalThreadLocalMap 的对应数组下标中取就完事了,不像传统的 ThreadLocal里的 ThreadLocalMap 进行 Hash计算得到,然后再取。

那么 0号 索引存放的是什么呢?索引0位置存放FastThreadLocal的Set集合,存放FastThreadLocal的引用, 更容易解决内存泄漏的问题。

2.3.3  FastThreadLocal get方法

 get() 方法如下:

public class FastThreadLocal<V> {
    // 这个就是索引为0 的初始化
    private static final int variablesToRemoveIndex = InternalThreadLocalMap.nextVariableIndex();
    // FastThreadLocal中的index是记录了该它维护的数据应该存储的位置
    private final int index;
    public FastThreadLocal() {
        // index 初始化 这里就从 1 开始了 依次递增
        index = InternalThreadLocalMap.nextVariableIndex();
    }
    public final V get() {
        // 获取当前线程的InternalThreadLocalMap
        InternalThreadLocalMap threadLocalMap = InternalThreadLocalMap.get();
        // 根据当前线程的index从InternalThreadLocalMap中获取其绑定的数据
        Object v = threadLocalMap.indexedVariable(index);
        // 如果获取当前线程绑定的数据不为缺省值UNSET,则直接返回;否则进行初始化
        if (v != InternalThreadLocalMap.UNSET) {
            return (V) v;
        }
        return initialize(threadLocalMap);
    }
    // ...
} 

可以看到:

(1)通过InternalThreadLocalMap.get()方法获取当前线程的InternalThreadLocalMap。

(2)根据当前线程的index 从InternalThreadLocalMap中获取其绑定的数据。

(3)如果不是缺省值UNSET,直接返回;如果是缺省值,则执行initialize方法进行初始化。

下面我们继续分析一下InternalThreadLocalMap.get()方法的实现逻辑:

(1)首先判断当前线程是否是FastThreadLocalThread类型,如果是FastThreadLocalThread类型则直接使用fastGet方法获取InternalThreadLocalMap,如果不是FastThreadLocalThread类型则使用slowGet方法获取InternalThreadLocalMap兜底处理。

(2)兜底处理中的slowGet方法会退化成JDK原生的ThreadLocal获取InternalThreadLocalMap。

(3)获取InternalThreadLocalMap时,如果为null,则会直接创建一个InternalThreadLocalMap返回。其创建过过程中初始化一个32位长度的Object数组,并将其元素全部设置为缺省值UNSET。

public final class InternalThreadLocalMap extends UnpaddedInternalThreadLocalMap 
{
    private static final int INDEXED_VARIABLE_TABLE_INITIAL_SIZE = 32;
 
    // 未赋值的Object变量(缺省值),当⼀个与线程绑定的值被删除之后,会被设置为UNSET
    public static final Object UNSET = new Object();
 
    // 存储绑定到当前线程的数据的数组
    private Object[] indexedVariables;
 
    // slowThreadLocalMap为JDK ThreadLocal存储InternalThreadLocalMap
    private static final ThreadLocal<InternalThreadLocalMap> slowThreadLocalMap = new ThreadLocal<InternalThreadLocalMap>();
 
    // 从绑定到当前线程的数据的数组中取出index位置的元素
    public Object indexedVariable(int index) {
        Object[] lookup = indexedVariables;
        return index < lookup.length? lookup[index] : UNSET;
    }
 
    public static InternalThreadLocalMap get() {
        Thread thread = Thread.currentThread();
        // 判断当前线程是否是FastThreadLocalThread类型
        if (thread instanceof FastThreadLocalThread) {
            return fastGet((FastThreadLocalThread) thread);
        } else {
            return slowGet();
        }
    }
 
    private static InternalThreadLocalMap fastGet(FastThreadLocalThread thread) {
        // 直接获取当前线程的InternalThreadLocalMap
        InternalThreadLocalMap threadLocalMap = thread.threadLocalMap();
        // 如果当前线程的InternalThreadLocalMap还未创建,则创建并赋值
        if (threadLocalMap == null) {
            thread.setThreadLocalMap(threadLocalMap = new InternalThreadLocalMap());
        }
        return threadLocalMap;
    }
 
    private static InternalThreadLocalMap slowGet() {
        // 使用JDK ThreadLocal获取InternalThreadLocalMap
        InternalThreadLocalMap ret = slowThreadLocalMap.get();
        if (ret == null) {
            ret = new InternalThreadLocalMap();
            slowThreadLocalMap.set(ret);
        }
        return ret;
    }
 
    private InternalThreadLocalMap() {
        indexedVariables = newIndexedVariableTable();
    }
 
    // 初始化一个32位长度的Object数组,并将其元素全部设置为缺省值UNSET
    private static Object[] newIndexedVariableTable() {
        Object[] array = new Object[INDEXED_VARIABLE_TABLE_INITIAL_SIZE];
        Arrays.fill(array, UNSET);
        return array;
    }
    // ...
}

所以当使用 FastThreadLocalThread 和 非 FastThreadLocalThread 的区别如下:

2.3.4  FastThreadLocal set方法

set() 方法如下:

public class FastThreadLocal<V> {
    // FastThreadLocal初始化时variablesToRemoveIndex被赋值为0
    private static final int variablesToRemoveIndex = InternalThreadLocalMap.nextVariableIndex();
 
    public final void set(V value) {
        // 判断value值是否是未赋值的Object变量(缺省值)
        if (value != InternalThreadLocalMap.UNSET) {
            // 获取当前线程对应的InternalThreadLocalMap
            InternalThreadLocalMap threadLocalMap = InternalThreadLocalMap.get();
            // 将InternalThreadLocalMap中数据替换为新的value
            // 并将FastThreadLocal对象保存到待清理的Set中
            setKnownNotUnset(threadLocalMap, value);
        } else {
            remove();
        }
    }
    private void setKnownNotUnset(InternalThreadLocalMap threadLocalMap, V value) {
        // 将InternalThreadLocalMap中数据替换为新的value
        if (threadLocalMap.setIndexedVariable(index, value)) {
            // 并将当前的FastThreadLocal对象保存到待清理的Set中
            addToVariablesToRemove(threadLocalMap, this);
            variablesToRemove.add(variable);
        }
    }
    // ...
}

可以看到:

(1)判断value是否是缺省值UNSET,如果value不等于缺省值,则会通过InternalThreadLocalMap.get()方法获取当前线程的InternalThreadLocalMap。

(2)通过FastThreadLocal中的setKnownNotUnset()方法将InternalThreadLocalMap中数据替换为新的value,并将当前的FastThreadLocal对象保存到待清理的Set中。

(3)如果等于缺省值UNSET或null(else的逻辑),会调用remove()方法,remove()具体见后面的代码分析。

接下来我们看下InternalThreadLocalMap.setIndexedVariable方法的实现逻辑:

(1)判断index是否超出存储绑定到当前线程的数据的数组indexedVariables的长度,如果没有超出,则获取index位置的数据,并将该数组index位置数据设置新value。

(2)如果超出了,绑定到当前线程的数据的数组需要扩容,则扩容该数组并将它index位置的数据设置新value。

(3)扩容数组以index 为基准进行扩容,将数组扩容后的容量向上取整为 2 的次幂。然后将原数组内容拷贝到新的数组中,空余部分填充缺省值UNSET,最终把新数组赋值给 indexedVariables。

public final class InternalThreadLocalMap extends UnpaddedInternalThreadLocalMap {
    // 未赋值的Object变量(缺省值),当⼀个与线程绑定的值被删除之后,会被设置为UNSET
    public static final Object UNSET = new Object();
    // 存储绑定到当前线程的数据的数组
    private Object[] indexedVariables;
    // 绑定到当前线程的数据的数组能再次采用x2扩容的最大量
    private static final int ARRAY_LIST_CAPACITY_EXPAND_THRESHOLD = 1 << 30;
    private static final int ARRAY_LIST_CAPACITY_MAX_SIZE = Integer.MAX_VALUE - 8;
 
    // 将InternalThreadLocalMap中数据替换为新的value
    public boolean setIndexedVariable(int index, Object value) {
        Object[] lookup = indexedVariables;
        if (index < lookup.length) {
            Object oldValue = lookup[index];
            // 直接将数组 index 位置设置为 value,时间复杂度为 O(1)
            lookup[index] = value;
            return oldValue == UNSET;
        } else { // 绑定到当前线程的数据的数组需要扩容,则扩容数组并数组设置新value
            expandIndexedVariableTableAndSet(index, value);
            return true;
        }
    }
 
    private void expandIndexedVariableTableAndSet(int index, Object value) {
        Object[] oldArray = indexedVariables;
        final int oldCapacity = oldArray.length;
        int newCapacity;
        // 判断可进行x2方式进行扩容
        if (index < ARRAY_LIST_CAPACITY_EXPAND_THRESHOLD) {
            newCapacity = index;
            // 位操作,提升扩容效率
            newCapacity |= newCapacity >>>  1;
            newCapacity |= newCapacity >>>  2;
            newCapacity |= newCapacity >>>  4;
            newCapacity |= newCapacity >>>  8;
             newCapacity |= newCapacity >>> 16;
            newCapacity ++;
        } else { // 不支持x2方式扩容,则设置绑定到当前线程的数据的数组容量为最大值
            newCapacity = ARRAY_LIST_CAPACITY_MAX_SIZE;
        }
        // 按扩容后的大小创建新数组,并将老数组数据copy到新数组
        Object[] newArray = Arrays.copyOf(oldArray, newCapacity);
        // 新数组扩容后的部分赋UNSET缺省值
        Arrays.fill(newArray, oldCapacity, newArray.length, UNSET);
        // 新数组的index位置替换成新的value
        newArray[index] = value;
        // 绑定到当前线程的数据的数组用新数组替换
        indexedVariables = newArray;
    }
    // ...
}

下面我们再继续看下FastThreadLocal.addToVariablesToRemove方法的实现逻辑。

(1)取下标index为0的数据(用于存储待清理的FastThreadLocal对象Set集合中),如果该数据是缺省值UNSET或null,则会创建FastThreadLocal对象Set集合,并将该Set集合填充到下标index为0的数组位置。

(2)如果该数据不是缺省值UNSET,说明Set集合已金被填充,直接强转获取该Set集合。

(3)最后将FastThreadLocal对象保存到待清理的Set集合中。

private static void addToVariablesToRemove(InternalThreadLocalMap threadLocalMap, FastThreadLocal<?> variable) {
    // 取下标index为0的数据,用于存储待清理的FastThreadLocal对象Set集合中
    Object v = threadLocalMap.indexedVariable(variablesToRemoveIndex);
    Set<FastThreadLocal<?>> variablesToRemove;
    if (v == InternalThreadLocalMap.UNSET || v == null) {
        // 下标index为0的数据为空,则创建FastThreadLocal对象Set集合
        variablesToRemove = Collections.newSetFromMap(new IdentityHashMap<FastThreadLocal<?>, Boolean>());
        // 将InternalThreadLocalMap中下标为0的数据,设置成FastThreadLocal对象Set集合
        threadLocalMap.setIndexedVariable(variablesToRemoveIndex, variablesToRemove);
    } else {
        variablesToRemove = (Set<FastThreadLocal<?>>) v;
    }
    // 将FastThreadLocal对象保存到待清理的Set中
    variablesToRemove.add(variable);
}

2.4  FastThreadLocal 的回收

FastThreadLocal 提供了三种回收机制:

自动: 使用FastThreadLocal执行一个被FastThreadLocalRunnable wrap的Runnable任务,在任务执行完毕后会自动进行ftl的清理。

手动: ftl和InternalThreadLocalMap都提供了remove方法,在合适的时候用户可以(有的时候也是必须,例如普通线程的线程池使用ftl)手动进行调用,进行显示删除。

关于自动: 为当前线程的每一个ftl注册一个Cleaner,当线程对象不强可达的时候,该Cleaner线程会将当前线程的当前ftl进行回收。(netty推荐如果可以用其他两种方式,就不要再用这种方式,因为需要另起线程,耗费资源,而且多线程就会造成一些资源竞争,在netty-4.1.34版本中,已经注释掉了调用ObjectCleaner的代码。)

2.5  FastThreadLocal 的优势

(1)减少内存占用:通过内存池化的方式,FastThreadLocal 避免了为每个线程局部变量创建和销毁实例的开销。

(2)提高性能:通过索引快速访问线程局部变量的副本,FastThreadLocal 减少了查找和修改线程局部变量的时间。

(3)简化编程模型:FastThreadLocal 提供了与标准 ThreadLocal 相似的 API,使得开发者可以在不改变编程习惯的前提下享受到性能提升。

3  小结

好啦,关于 FTL本节就看到这里,有理解不对的地方欢迎指正哈。

posted @ 2024-05-18 14:10  酷酷-  阅读(294)  评论(0编辑  收藏  举报