Netty-FastThreadLocal

 ThreadLocal:

  ThreadLocalMap中实际存储键值对的是Entry[]数组,Entry对象的Key为ThreadLocal对象,value为线程持有的本地变量。

  Entry继承了WeakReference<ThreadLocal<?>>,所以Entry对象是其key即ThreadLocal对象的一个弱引用,这种设计是为了当ThreadLocal对象被置为null时,Entry对象会在第二次YoungGC被回收,线程本地变量value也会被回收。

  不足:

    ①:然而ThreadLocal对象一般是静态的,因此Entry对象不会被GC回收,如果没有在代码中手动 remove的话会导致内存泄露。

    ②:ThreadLocalMap是使用线性探测的方式解决hash冲突的,如果没有找到空闲的slot,就不断往后尝试,直到找到一个空闲的位置来插入entry,这种方式在经常遇到hash冲突时,影响效率

 

  ThreadLocal博客:https://www.cnblogs.com/yangyongjie/p/10574591.html

 

 

 

  FastThreadLocal:

  Netty为了提高性能,改进了JDK ThreadLocal。

  Netty实现的FastThreadLocal优化了Java原生ThreadLocal的访问速度,存储速度,避免了检测弱引用带来的线程本地变量value垃圾回收的问题,及数组位置冲突带来的线性查找问题。

  

  JDK和Netty类之间对应关系:

JDK Netty 说明
Thread FastThreadLocalThread FastThreadLocalThread是Thread的子类
ThreadLocal FastThreadLocal FastThreadLocal用于操作FastThreadLocalThread中的InternalThreadLocalMap
ThreadLocalMap InternalThreadLocalMap FastThreadLocalThread中的私有属性

  FastThreadLocalThread是对JDK Thread类的一层包装,每个线程对应一个InternalThreadLocalMap实例。

public class FastThreadLocalThread extends Thread {
    // 用于存储线程本地变量,代替ThreadLocalMap
    private InternalThreadLocalMap threadLocalMap;
    ...
}

    对应关系图:  

     

 

 

 

  FastThreadLocal使用示例:

import io.netty.util.concurrent.FastThreadLocal;

/**
 * Netty FastThreadLocal保存的本地变量
 * 只有在 FastThreadLocalThread 线程中调用才会使用FastThreadLocal的逻辑,否则会使用ThreadLocal
 *
 */
public class FastContext {

    private static FastThreadLocal<FastContext> FAST_THREAD_LOCAL = new FastThreadLocal<FastContext>() {
        @Override
        protected FastContext initialValue() throws Exception {
            return new FastContext();
        }
    };

    public static FastContext currentContext() {
        return FAST_THREAD_LOCAL.get();
    }

    public static void remove() {
        FAST_THREAD_LOCAL.remove();
    }

    /**
     * 线程追踪ID
     */
    private String thraceId;

    public String getThraceId() {
        return thraceId;
    }

    public void setThraceId(String thraceId) {
        this.thraceId = thraceId;
    }

}

   写入本地变量:

    public static void main(String[] args) {
        // Netty提供的DefaultThreadFactory创建的线程是FastThreadLocalThread类型的
        DefaultThreadFactory NettyFastThreadFactory = new DefaultThreadFactory("FastThreadTest");
        NettyFastThreadFactory.newThread(new Runnable() {
            @Override
            public void run() {
                FastContext fastContext = FastContext.currentContext();
                String uuid = UUID.randomUUID().toString();
                System.out.println(uuid); // 27688ce3-4786-4197-b072-ade47bd5061a
                fastContext.setThraceId(uuid);

                testFastThreadLocal();
            }
        }).start();

    }

    private static void testFastThreadLocal() {
        FastContext fastContext = FastContext.currentContext();
        System.out.println(fastContext.getThraceId()); // 27688ce3-4786-4197-b072-ade47bd5061a
        FastContext.remove();
    }

 

 

 

  FastThreadLocal如何解决hash冲突的?

    使用Object[]数组来保存本地变量value,每个FastThreadLocal实例对应的value都保存在数组的同一下标中。

    ①:每一个FastThreadLocal实例创建时,分配一个下标Index(使用AtomicInteger来保证顺序递增),每个FastThreadLocal实例都能获取一个不重复的下标。

    ②:FastThreadLocal实例保存了其被分配的下标Index,所有使用同一个FastThreadLocal实例保存的本地变量value对应的下标都相同,都是Index(即所有FastThreadLocalThread使用同一个FastThreadLocal实例来保存的本地变量value,都在其线程中InternalThreadLocalMap中Object[]数组的同一下标中

    ③:当调用get()方法时,直接根据FastThreadLocal实例保存的index,去Object[]数组中去获取value,时间复杂度为O(1)

 

    如果程序中有多个FastThreadLocal实例,那么数组下标递增到非常大,数组也会比较大。虽然数组较大,但是避免了在ThreadLocal中计算索引下标位置及处理Hash冲突带来的损耗,所以在操作数组时使用固定下标比使用哈希下标有一定的性能优势,特别是在拼单使用时会非常显著,所以 FastThreadLocal 是通过空间换时间的思想提升读写性能

 

   分配数组下标的逻辑:

    ①:FastThreadLocal 中的常量index,用于保存其在实例化时获取到的递增的数组下标

    private final int index;

    public FastThreadLocal() {
        index = InternalThreadLocalMap.nextVariableIndex();
    }

    ②:InternalThreadLocalMap

    每个FastThreadLocal实例化时,index+1,有多少个FastThreadLocal实例,index就为多大

    // 用于生成数组下标    
    private static final AtomicInteger nextIndex = new AtomicInteger();
    // 生成数组下标
    public static int nextVariableIndex() {
        int index = nextIndex.getAndIncrement();
        if (index < 0) {
            nextIndex.decrementAndGet();
            throw new IllegalStateException("too many thread-local indexed variables");
        }
        return index;
    }

    ③:保存本地变量value时,根据FastThreadLocal中的index将value保存到指定的数组下标中

    private void setKnownNotUnset(InternalThreadLocalMap threadLocalMap, V value) {
        // 将value保存在当前FastThreadLocalThread的InternalThreadLocalMap的Object[]的index下标中
        if (threadLocalMap.setIndexedVariable(index, value)) {
            addToVariablesToRemove(threadLocalMap, this);
        }
    }

 

  问题:

    ①:递增的index会不会非常大,导致数组也非常大?

      不会,每种数据类型的线程本地变量只需要定义一个ThreadLocal即可,所有的线程共享这个ThreadLocal实例,所以线程本地变量的数据类型有几种,index最大就为多大,一般在个位数。

 

 

  FastThreadLocal部分源码分析:

    set():

    public final void set(V value) {
        // 如果value不等于UNSET值,则保存
        if (value != InternalThreadLocalMap.UNSET) {
            // 获取当前线程的InternalThreadLocalMap
            InternalThreadLocalMap threadLocalMap = InternalThreadLocalMap.get();
            // 将Map中对应下标的数据替换成value
            setKnownNotUnset(threadLocalMap, value);
        } else {
            remove();
        }
    }

    get():

    public final V get() {
        // 获取当前线程的InternalThreadLocalMap
        InternalThreadLocalMap threadLocalMap = InternalThreadLocalMap.get();
        // 根据当前FastThreadLocal的index常量获取对应下标的value
        Object v = threadLocalMap.indexedVariable(index);
        // 如果value不等于数组中的UNSET值(可以理解成默认值),则返回value
        if (v != InternalThreadLocalMap.UNSET) {
            return (V) v;
        }
        // 将initialValue保存到Map的index下标中并返回
        return initialize(threadLocalMap);
    }

 

 

 

   总结:

    ①:FastThreadLocal只在FastThreadLocalThread线程中使用才能发挥其性能,在Thread线程中使用的还是ThreadLocal

    ②:当对一个线程设置的线程本地变量不多的情况下,ThreadLocal的性能比FastThreadLocal高,所以一般情况下,使用ThreadLocal足矣

 

 

 

END.

posted @ 2022-03-22 09:35  杨岂  阅读(231)  评论(0编辑  收藏  举报