在Netty中使用FastThreadLocal代替ThreadLocal

FastThreadLocal是Netty中优化线程局部变量的一种方式,相比ThreadLocal,它通过数组而非哈希表提高查找效率。每个FastThreadLocal在创建时会分配一个全局递增的index,使得在FastThreadLocalThread中快速定位存储对象。FastThreadLocalThread内部包含一个InternalThreadLocalMap,通过这个映射,FastThreadLocal能够直接访问并存储数据,避免了哈希计算的时间开销。当InternalThreadLocalMap空间不足时,会进行扩容。FastThreadLocal的使用减少了性能损耗,尤其适用于频繁访问的场景。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

FastThreadLocal相比较于ThreadLocal在FastThreadLocalThread有更好的表现,因为在FastThreadLocal是使用数组而不是像ThreadLocal那样使用hash code 以及hash table去查找对象。尽管看起来非常微妙,但它比使用哈希表产生了一些性能优势,并且在频繁访问时非常有用。

为了使用FastThreadLocal带来的优势,你的线程类型应该使用FastThreadLocal以及它的子类,而不是使用Thread(下面会讲到原因)。因此在Netty中,所有的线程创建都是通过DefaultThreadFactory,因为其默认返回FastThreadLocalThread。

1、FastThreadLocal使用方法

在之前讲到Netty的Recycler的时候,用到了FastThreadLocal存储为其他线程回收的对象,也就是域FastThreadLocal<Map<Stack<?>,WeakOrderQueue>> DELAYED_RECYCLED

    private static final FastThreadLocal<Map<Stack<?>, WeakOrderQueue>> DELAYED_RECYCLED =
            new FastThreadLocal<Map<Stack<?>, WeakOrderQueue>>() {
        @Override
        protected Map<Stack<?>, WeakOrderQueue> initialValue() {
            return new WeakHashMap<Stack<?>, WeakOrderQueue>();
        }
    };

从这里可以看到FastThreadLocal本身不是直接实现这个对象,而是通过继承FastThreadLocal并且实现内部的initalValue()方法,在没有对象返回时创建对象。

2、InternalThreadLocalMap和FastThreadLocal内部联系

ThreadLocal本身只是一个窗口,用于访问Thread内部自己的ThreadLocalMap。而FastThreadLocal也是一样,用于访问FastThreadLocalThread内部的InternalThreadLocalMap,所以我们在开始讲解FastThreadLocal前,得先开始介绍InternalThreadLocalMap这个类在FastThreadLocal的使用。

    /* FastThreadLocal */
    private final int index;

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

每个FastThreadLocal在创建的时候都会有一个index的域,该值会在创建FastThreadLocal时通过c.nextVariableIndex()方法返回生成。

    /* InternalThreadLocalMap */
    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;
    }

nextIndex这个是在InternalThreadLocalMap类的一个静态属性,所以该值为JVM中所有InternalThreadLocalMap共享。这也可以说明在InternalThreadLocalMap.nextVariableIndex()返回给FastThreadLocal的index值是全局递增的。

随后通过FastThreadLocal.get()方法获取属性时,会在InternalThreadLocalMap内部的数组指定的下标index获取。

    /* FastThreadLocal */
    public final V get() {
        InternalThreadLocalMap threadLocalMap = InternalThreadLocalMap.get();
        Object v = threadLocalMap.indexedVariable(index);
        if (v != InternalThreadLocalMap.UNSET) {
            return (V) v;
        }

        return initialize(threadLocalMap);
    }

因为是通过数组下标直接获取对象,比起ThreadLocal内部需要使用hash的方式节约了不小的时间。

3、InternalThreadLocalMap源码解析

简单解释了InternalThreadLocalMap和FastThreadLocal的关系,我们看是讲解下InternalThreadLocalMap的源码。

InternalThreadLocalMap本身继承了UnpaddedInternalThreadLocalMap,UnpaddedInternalThreadLocalMap本身提供了Object类型的数组,在已知存储对象对应下标的情况下可以直接获取到对象。

class UnpaddedInternalThreadLocalMap {

    static final ThreadLocal<InternalThreadLocalMap> slowThreadLocalMap = new ThreadLocal<InternalThreadLocalMap>();
    static final AtomicInteger nextIndex = new AtomicInteger();

    /** Used by {@link FastThreadLocal} */
    Object[] indexedVariables;
    ...
}

上面我们看到了可以在FastTheadLocal中,通过InternalTheadLocalMap.get()静态方法返回存储在一个InternalThreadLocalMap的实例。其内部实现为

    /* InternalThreadLocalMap  */
    public static InternalThreadLocalMap get() {
        Thread thread = Thread.currentThread();
        if (thread instanceof FastThreadLocalThread) {
            return fastGet((FastThreadLocalThread) thread);
        } else {
            return slowGet();
        }
    }

get的方法会根据当前线程的是否继承了FastThreadLocalThred采取fastGet和slowGet两种方式。我们先来看fastGet这种

    /* InternalThreadLocalMap */
    private static InternalThreadLocalMap fastGet(FastThreadLocalThread thread) {
        InternalThreadLocalMap threadLocalMap = thread.threadLocalMap();
        if (threadLocalMap == null) {
            thread.setThreadLocalMap(threadLocalMap = new InternalThreadLocalMap());
        }
        return threadLocalMap;
    }

fastGet()就是比较简单的从FastThreadLocalThread中直接返回InternalThreadLocalMap,并且判断该值为null再初始化。现在我们再来看下slowGet的情况

    /* UnpaddedInternalThreadLocalMap */
    static final ThreadLocal<InternalThreadLocalMap> slowThreadLocalMap = new ThreadLocal<InternalThreadLocalMap>();

    /* InternalThreadLocalMap */
    private static InternalThreadLocalMap slowGet() {
        ThreadLocal<InternalThreadLocalMap> slowThreadLocalMap = UnpaddedInternalThreadLocalMap.slowThreadLocalMap;
        InternalThreadLocalMap ret = slowThreadLocalMap.get();
        if (ret == null) {
            ret = new InternalThreadLocalMap();
            slowThreadLocalMap.set(ret);
        }
        return ret;
    }

slowGet()的调用是发生在线程本身没有继承FastThreadLocalThread的情况下,是原生的Thread类型下,此时slowGet()就会去原生线程的ThreadLocal中获取slowThreadLocalMap域。

所以我们可以就此得出InternalThreadLocalMap中slowGet()和fastGet()区别在于,fastGet()的使用场景是当前线程是FastThreadLocalThread的,所以可以直接返回InternalThreadLocalMap这个域;而slowGet()是从ThreadLocal中返回的InternalThreadLocalMap,需要通过hash计算以及存储时的hash碰撞,所以相比较会慢一些。

既然获取到InternalThreadLocalMap,那么就可以通过创建FastThreadLocal分发的index获取到存储在其中的对象。

    public Object indexedVariable(int index) {
        Object[] lookup = indexedVariables;
        return index < lookup.length? lookup[index] : UNSET;
    }

简单看出:InternalTheadLocalMap改用数组的方式替换了ThreadLocalMap中用Hash计算的问题,并且避免了Hash碰撞带来的影响。

3、FastThreadLocalThread源码解析

FastThreadLocalThread本身继承了Thread,所以可以当作普通线程一样去使用。在这里它和普通Thread的区别在于内置了一个InternalThreadLocalMap,这也是FastThreadLocal获取InternalThreadLocalMap中fastGet和slowGet的区别。

public class FastThreadLocalThread extends Thread {

    private InternalThreadLocalMap threadLocalMap;

    ...
    public final InternalThreadLocalMap threadLocalMap() {
        return threadLocalMap;
    }

    public final void setThreadLocalMap(InternalThreadLocalMap threadLocalMap) {
        this.threadLocalMap = threadLocalMap;
    }

}

4、FastThreadLocal源码解析

FastThreadLocal作为Netty首选的线程的本地存储,主要用于搭配InternalThreadLocalMap。其实无论时FastTheadLocal还是ThreadLocal,它本身其实只是一个去访问线程内置的线程本地存储的窗口。例如TheadLocal,其实是访问Thead内置的ThreadLocalMap,而FastThreadLocal则是访问InternalThreadLocalMap。

每一个FastThreadLocalThread作为访问线程本地变量的接口,基本都会被定义为static + final类型的。

    private static final FastThreadLocal<Map<Stack<?>, WeakOrderQueue>> DELAYED_RECYCLED =
            new FastThreadLocal<Map<Stack<?>, WeakOrderQueue>>() {
        @Override
        protected Map<Stack<?>, WeakOrderQueue> initialValue() {
            return new WeakHashMap<Stack<?>, WeakOrderQueue>();
        }
    };

每个FastThreadLocal在初始化的时候,都会通过InternalThreadLocalMap分配一个递增的index。

    private final int index;

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

关键就是在这个index,这个index是final的,而且FastThreadLocal是static的,结合前面看到InternalThreadLocalMap内部存储的数组,而且每个FastThreadLocal只能指向一个对象,可以知道,每个线程通过同一个FastThreadLocal存储的对象,其存储在自身的InternalThreadLocalMap的位置都是一样的。具体可以看到下面例子

    public static void main(String[] args) {
        final FastThreadLocal<Integer> fastThreadLocal = new FastThreadLocal<Integer>();

        final FastThreadLocal<Integer> fastThreadLocal2 = new FastThreadLocal<Integer>();
        
        new FastThreadLocalThread(new Runnable() {
            @Override
            public void run() {
                try {
                    TimeUnit.SECONDS.sleep(2);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                fastThreadLocal2.set(12);
                fastThreadLocal.set(13);
                try {
                    Field field = FastThreadLocalThread.class.getDeclaredField("threadLocalMap");
                    field.setAccessible(true);
                    InternalThreadLocalMap internalThreadLocalMap = (InternalThreadLocalMap) field.get(Thread.currentThread());
                    System.out.println(internalThreadLocalMap);
                } catch (NoSuchFieldException e) {
                    e.printStackTrace();
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                }

            }
        }).start();
    }

debug的结果

可以看到fastThreadLocal2晚于fastThreadLocal创建,所以被InternalThreadLocalMap分配的index小于fastThreadLocal,所以fastThreadLocal2即使在线程中调用set方法早于fastThreadLocal,也只会放在index=3的位置,而fastThreadLocal的数据则放在index=2的位置。同时我们再来看下一个例子

public static void main(String[] args) {
        final FastThreadLocal<Integer> fastThreadLocal1 = new FastThreadLocal<Integer>();

        final FastThreadLocal<Integer> fastThreadLocal2 = new FastThreadLocal<Integer>();

        final FastThreadLocal<Integer> fastThreadLocal3 = new FastThreadLocal<Integer>();

        final FastThreadLocal<Integer> fastThreadLocal4 = new FastThreadLocal<Integer>();

        final FastThreadLocal<Integer> fastThreadLocal5 = new FastThreadLocal<Integer>();

        final FastThreadLocal<Integer> fastThreadLocal6 = new FastThreadLocal<Integer>();

        new FastThreadLocalThread(new Runnable() {
            @Override
            public void run() {
                try {
                    TimeUnit.SECONDS.sleep(2);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                fastThreadLocal6.set(13);
                try {
                    Field field = FastThreadLocalThread.class.getDeclaredField("threadLocalMap");
                    field.setAccessible(true);
                    InternalThreadLocalMap internalThreadLocalMap = (InternalThreadLocalMap) field.get(Thread.currentThread());

                    Field field2 = UnpaddedInternalThreadLocalMap.class.getDeclaredField("indexedVariables");
                    field2.setAccessible(true);
                    Object[] indexedVariables = (Object[]) field2.get(internalThreadLocalMap);
                    int count = 0;
                    for(Object obj : indexedVariables) {
                        System.out.println(count++ + " ->" +  obj);
                    }
                } catch (NoSuchFieldException e) {
                    e.printStackTrace();
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                }

            }
        }).start();


        new FastThreadLocalThread(new Runnable() {
            @Override
            public void run() {
                try {
                    TimeUnit.SECONDS.sleep(2);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                fastThreadLocal6.set(12);
                try {
                    Field field = FastThreadLocalThread.class.getDeclaredField("threadLocalMap");
                    field.setAccessible(true);
                    InternalThreadLocalMap internalThreadLocalMap = (InternalThreadLocalMap) field.get(Thread.currentThread());

                    Field field2 = UnpaddedInternalThreadLocalMap.class.getDeclaredField("indexedVariables");
                    field2.setAccessible(true);
                    Object[] indexedVariables = (Object[]) field2.get(internalThreadLocalMap);
                    int count = 0;
                    for(Object obj : indexedVariables) {
                        System.out.println(count++ + " ->" +  obj);
                    }
                } catch (NoSuchFieldException e) {
                    e.printStackTrace();
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                }

            }
        }).start();
    }

输出结果

4 ->java.lang.Object@37348c65
7 ->13
5 ->java.lang.Object@37348c65
6 ->java.lang.Object@37348c65
8 ->java.lang.Object@37348c65
9 ->java.lang.Object@37348c65
10 ->java.lang.Object@37348c65
11 ->java.lang.Object@37348c65
12 ->java.lang.Object@37348c65
13 ->java.lang.Object@37348c65
14 ->java.lang.Object@37348c65
15 ->java.lang.Object@37348c65
16 ->java.lang.Object@37348c65
17 ->java.lang.Object@37348c65
18 ->java.lang.Object@37348c65
7 ->12
8 ->java.lang.Object@37348c65
9 ->java.lang.Object@37348c65

可以看到两个FastThreadLocalThread通过变量fastThreadLocal6存放的数据,都是存放在自身InternalThreadLocalMap的同一个位置,因为这个位置在FastThreadLocal创建时就被定义。总结起来就是:                              FastThreadLocal在创建之后,所有FastThreadLocalThread调用这个对象并且存入数据时,存在在自身的InternalThreadLocalMap的位置都一样。

同样的,其实我们也会发现因为本身是数组,所以也会带来一定的空间浪费,这明显是一种时间换空间的方法。所以在内部的InternalThreadLocalMap的数组中,会用一个空对象占位来尽可能的减小空间的使用。

5、InternalThreadLocalMap的数组扩容

FastThreadLocal存入数据时,如果发现InternalThreadLocalMap的空间不够,则会尝试扩容。

    public boolean setIndexedVariable(int index, Object value) {
        // 数据都是存储在indexedVariables 数组里面,所以是Object
        Object[] lookup = indexedVariables;
        if (index < lookup.length) {
            Object oldValue = lookup[index];
            lookup[index] = value;
            return oldValue == UNSET;
        } else {
            expandIndexedVariableTableAndSet(index, value);
            return true;
        }
    }

    private void expandIndexedVariableTableAndSet(int index, Object value) {
        Object[] oldArray = indexedVariables;
        final int oldCapacity = oldArray.length;
        int newCapacity = index;
        newCapacity |= newCapacity >>>  1;
        newCapacity |= newCapacity >>>  2;
        newCapacity |= newCapacity >>>  4;
        newCapacity |= newCapacity >>>  8;
        newCapacity |= newCapacity >>> 16;
        newCapacity ++;

        Object[] newArray = Arrays.copyOf(oldArray, newCapacity);
        Arrays.fill(newArray, oldCapacity, newArray.length, UNSET);
        newArray[index] = value;
        indexedVariables = newArray;
    }

代码比较简单,其中位运算参的最终目的就是扩容一倍。

// 9 换算称2进制为1001
1001  |= 1001  >>> 1; // 结果为1101
1101  |= 1101  >>> 2; // 结果为10000
10000 |= 10000 >>> 4; // 结果为10001 = 17

6、伪共享

参考文章:Netty 之线程本地变量 FastThreadLocal

7、参考

图解netty:FastThreadLocal实现原理分析

 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值