【战、面试官】小伙子告诉我,为什么说FastThreadLocal是装上火箭的ThreadLocal?

766 阅读6分钟

我是「 kangarooking(袋鼠帝) 」,不是弟弟的弟。真心给大家分享经验和技术干货,面试次数100+。面试经验绝对丰富,也当过面试官,内容绝对用心可靠。点赞在看,养成习惯。关注me,每天进步亿点点 ❗ ❗ ❗

前言

文章的开头先聊一下为什么最近很久都没有更新了,我们公司需要考一个证,具体什么证就不说了。但是因为疫情,这个考试一直在推迟,之前本来二月份有一次考试的,但是临考之前两天又通知延期了(延期到5月底)。三月份接到公司说考不过就要求离职的通知,压力也比较大,一直在准备考试的内容,也就是下一次考试是我最后的机会。时间到了五月初又被通知延期了,目前还不知道下一次考试时间是什么时候,还是有点焦虑的。趁着这段时间不用备考,我赶紧捡起写文章这个事情。

再说一个感悟吧,最近接到公司的一个任务,参与公司科技宣传视频的拍摄,分到一个组的小伙伴有一半都是国外毕业回来的应届生。感慨有钱确实不一样,我出来打拼了几年进的公司,人家毕业就可以进了,起点确实高(当然不是说所有的国外读书的小伙伴都是靠有钱哈,不会以偏概全。当然有条件的小伙伴真的可以出国留学一下,读万卷书,不如行万里路嘛)穷人真的只有靠不断的努力和正确的选择才能改变命运。当然自己还是要保持自信!对自己有正确的认知,知道自己需要什么,接下来要做什么,规划好自己的人生。跟自己比较,少和别人去比较,如果今天的自己比昨天又更进了一步,这何尝不是一种成功呢。

好了我一个理科生,自从上大学之后就再也没有触碰过文学语文这方面了。就不扯太多了,毕竟文笔也不好。只求大家看得懂就行~~

阅读本篇文章前需要对ThreadLocal有一定的了解

FastThreadLocal相关类: FastThreadLocalRunnable,FastThreadLocal,InternalThreadLocalMap,FastThreadLocalThread;

与ThreadLocal类比:

FastThreadLocalThreadLocal描述
FastThreadLocalRunnableRunnable一个可执行任务
InternalThreadLocalMapThreadLocalMap线程的成员变量
FastThreadLocalThreadThread线程

实现方式的不同

ThreadLocal的结构: ThreadLocal是在Thread类中增加一个成员变量ThreadLocalMap(是一个keyvalue存储的结构)key是某个ThreadLocal对象,value就是该ThreadLocal对应的值:

    ThreadLocal<String> stringThreadLocal = new ThreadLocal<>();
    stringThreadLocal.set("kangarooKing");
    String s = stringThreadLocal.get();
        
    /**************************************/
    public T get() {
        Thread t = Thread.currentThread();
        //获取当前线程中的ThreadLocalMap变量
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            //通过this也就是当前的ThreadLocal对象为key获取value
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }

ThreadLocalMap的数据结构和HashMap有些类似,好奇的小伙伴自行学习,本篇主要讲FashThreadLocal

FastThreadLocal的结构: InternalThreadLocalMap对标ThreadLocalMapFastThreadLocalThread(对标Thread)的成员变量,这个map它内部结构其实就是一个Object[]数组,数组里面的值就是value,那么怎么获取value呢,肯定只能是下标了。InternalThreadLocalMap中有一个:

private static final AtomicInteger nextIndex = new AtomicInteger();

它就是用来生成下标的,生成的下标赋值给FastThreadLocalThread的成员变量index。这样在FastThreadLocalThread.get()的时候就可以根据自身的index下标从当前线程的InternalThreadLocalMapObject[]数组中获取value了。index是在ThreadLocal的无参构造方法里面就赋值了,也就是说new一个新的FastThreadLocal对象就会自动获取到对应的index。从static final AtomicInteger可以看出nextIndex是一个静态变量,在程序里面每新建一个FastThreadLocal对象该值就会+1。

leiTu.png

比如:当前有线程1,线程2,线程1创建了一个FastThreadLocal对象index=1,线程2也创建了一个FastThreadLocal对象index=2,这时到线程3创建了,线程3在调用FastThreadLocal的无参构造方法时获取到的nextIndex值为3。

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

结论:FastThreadLocal对象通过创建时获取的index下标,在set时通过该下标set进当前线程的InternalThreadLocalMapObject[]中,get的时候也通过下标从当前线程的InternalThreadLocalMapObject[]中获取对应的value。 优点:

用于debug调试的代码:

import io.netty.util.concurrent.FastThreadLocal;
import io.netty.util.concurrent.FastThreadLocalThread;

/**
 * netty自定义的FastThreadLocal就是通过空间换时间的方式
 * 一个FastThreadLocalThread对应一个InternalThreadLocalMap,内部indexedVariables是一个Object[]数组来存储值
 * 一个FastThreadLocal对象代表一个下标,下标从1一次递增。
 * get的时候通过当前FastThreadLocal对象的index下标从,InternalThreadLocalMap的indexedVariables获取值,时间复杂度O1
 */
public class FastThreadLocalClient {
    public static void main(String[] args) {
        FastThreadLocal<Integer> fastThreadLocal = new FastThreadLocal<>();
        FastThreadLocal<Integer> fastThreadLocal1 = new FastThreadLocal<>();
        FastThreadLocal<Integer> fastThreadLocal2 = new FastThreadLocal<>();
        FastThreadLocal<Integer> fastThreadLocal3 = new FastThreadLocal<>();
        FastThreadLocal<Integer> fastThreadLocal4 = new FastThreadLocal<>();
        new FastThreadLocalThread(() -> {
            fastThreadLocal.set(5);
            fastThreadLocal1.set(6);
            fastThreadLocal2.set(7);
            fastThreadLocal3.set(8);
            fastThreadLocal4.set(9);
            fastThreadLocal4.remove();
            Integer integer1 = fastThreadLocal4.get();
            System.out.println(integer1);
            FastThreadLocal<Integer> fastThreadLocal6 = new FastThreadLocal<>();
            System.out.println("thread:" + Thread.currentThread().getName() + " 放入i=" + 5);
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            Integer integer = fastThreadLocal.get();
            System.out.println("thread:" + Thread.currentThread().getName() + " 取出i=" + integer);
        }).start();
        FastThreadLocal<Integer> fastThreadLocal9 = new FastThreadLocal<>();
    }
}

FastThreadLocal代码分析

1.get方法

FastThreadLocal内部代码

    private final int index;

    public FastThreadLocal() {
        index = InternalThreadLocalMap.nextVariableIndex();
    }
    
    public final V get() {
        //从当前线程中获取其成员变量InternalThreadLocalMap
        InternalThreadLocalMap threadLocalMap = InternalThreadLocalMap.get();
        //通过下标获取到值
        Object v = threadLocalMap.indexedVariable(index);
        if (v != InternalThreadLocalMap.UNSET) {
            return (V) v;
        }

        return initialize(threadLocalMap);
    }
    

InternalThreadLocalMap


    public static InternalThreadLocalMap get() {
        //获取当前线程
        Thread thread = Thread.currentThread();
        if (thread instanceof FastThreadLocalThread) {
            //如果当前线程属于FastThreadLocalThread,就走快速get逻辑
            return fastGet((FastThreadLocalThread) thread);
        } else {
            //如果当前线程属于普通线程就走jdk ThreadLocal的逻辑
            return slowGet();
        }
    }

    //这就是InternalThreadLocalMap中存储value的数组
    /** Used by {@link FastThreadLocal} */
    private Object[] indexedVariables;
    
    //根据下标获取数组中的value
    public Object indexedVariable(int index) {
        Object[] lookup = indexedVariables;
        return index < lookup.length? lookup[index] : UNSET;
    }

2.set方法

FastThreadLocal方法

    //FastThreadLocal中用于获取value的下标
    private final int index;

    public FastThreadLocal() {
        //在创建对象的时候为index赋值
        index = InternalThreadLocalMap.nextVariableIndex();
    }
    
    public final void set(V value) {
        //set前先判断value不是一个空节点值
        if (value != InternalThreadLocalMap.UNSET) {
            //获取当前线程的InternalThreadLocalMap
            InternalThreadLocalMap threadLocalMap = InternalThreadLocalMap.get();
            //set value
            setKnownNotUnset(threadLocalMap, value);
        } else {
            remove();
        }
    }
    
    private void setKnownNotUnset(InternalThreadLocalMap threadLocalMap, V value) {
        //将value放到对应的index下标
        if (threadLocalMap.setIndexedVariable(index, value)) {
            //上面的方法返回true执行该方法
            //该方法是用来将所有当前线程使用到的FastThreadLocal对象存储到
            //InternalThreadLocalMap的Object[]的0下标位置,也就是0下标位置的value是一个集合
            addToVariablesToRemove(threadLocalMap, this);
        }
    }

InternalThreadLocalMap内部方法

    //注释的意思当且仅当一个新的FastThreadLocal被创建时才会返回true
    /**
     * @return {@code true} if and only if a new thread-local variable has been created
     */
    public boolean setIndexedVariable(int index, Object value) {
        Object[] lookup = indexedVariables;
        if (index < lookup.length) {
            Object oldValue = lookup[index];
            lookup[index] = value;
            return oldValue == UNSET;
        } else {
            expandIndexedVariableTableAndSet(index, value);
            return true;
        }
    }

为什么用oldValue == UNSET来判断当前FastThreadLocal对象第一次被当前线程使用呢,因为InternalThreadLocalMap在初始化Object[]的时候直接将Object[]填满了UNSET对象。

    private static final int INDEXED_VARIABLE_TABLE_INITIAL_SIZE = 32;
    public static final Object UNSET = new Object();
    
    private InternalThreadLocalMap() {
        indexedVariables = newIndexedVariableTable();
    }
    
    private static Object[] newIndexedVariableTable() {
        Object[] array = new Object[INDEXED_VARIABLE_TABLE_INITIAL_SIZE];
        Arrays.fill(array, UNSET);
        return array;
    }

2.png

小结:FastThreadLocal的set方法将value根据index放入当前线程InternalThreadLocalMapObject[]相应位置,并且会判断当前FastThreadLocal是否第一次在该线程被使用,如果是的话就将该FastThreadLocal对象加入到Object[] 0下标位置的集合中。

注意:FastThreadLocal中的index是从1开始递增的。而0下标是在程序启动的时候就已经优先分配了(这个会在后面的防止内存泄漏讲解中提到)。

InternalThreadLocalMap中对FastThreadLocal index的计算,就是递增


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;
    }

3.扩容方式

其实也就是类似ArrayList扩容,先创建一个更大的新数组,然后将老数组copy进新数组。

    private void expandIndexedVariableTableAndSet(int index, Object value) {
        Object[] oldArray = indexedVariables;
        final int oldCapacity = oldArray.length;
        int newCapacity;
        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 {
            newCapacity = ARRAY_LIST_CAPACITY_MAX_SIZE;
        }

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

4.remove方法

FastThreadLocal方法

    public final void remove(InternalThreadLocalMap threadLocalMap) {
        if (threadLocalMap == null) {
            return;
        }

        Object v = threadLocalMap.removeIndexedVariable(index);
        removeFromVariablesToRemove(threadLocalMap, this);

        if (v != InternalThreadLocalMap.UNSET) {
            try {
                onRemoval((V) v);
            } catch (Exception e) {
                PlatformDependent.throwException(e);
            }
        }
    }


InternalThreadLocalMap内部方法

    public Object removeIndexedVariable(int index) {
        Object[] lookup = indexedVariables;
        if (index < lookup.length) {
            Object v = lookup[index];
            lookup[index] = UNSET;
            return v;
        } else {
            return UNSET;
        }
    }

5.避免内存泄漏的方式

还记得set中的这个方法吗

3.png

该方法用于将第一次在当前线程中被使用的FastThreadLocal对象放入Object[] 0下标的集合中,用于在当前线程执行完之后方便将所有该线程使用到的FastThreadLocal清理掉。 下面的代码属于FastThreadLocal类中。

    //这里就是分配Object[] 0下标的地方
    //因为是静态变量,所以在类加载的初始化阶段就会被执行且赋值,所以这里最先执行获取了0下标。
    private static final int variablesToRemoveIndex = InternalThreadLocalMap.nextVariableIndex();
    
    //所以index就只能从1获取。
    private final int index;

    @SuppressWarnings("unchecked")
    private static void addToVariablesToRemove(InternalThreadLocalMap threadLocalMap, FastThreadLocal<?> variable) {
        //从Object[]的0下标获取集合
        Object v = threadLocalMap.indexedVariable(variablesToRemoveIndex);
        Set<FastThreadLocal<?>> variablesToRemove;
        if (v == InternalThreadLocalMap.UNSET || v == null) {
            //如果0下标还没有集合对象就新建一个
            variablesToRemove = Collections.newSetFromMap(new IdentityHashMap<FastThreadLocal<?>, Boolean>());
            //将新建的集合对象赋值到0下标
            threadLocalMap.setIndexedVariable(variablesToRemoveIndex, variablesToRemove);
        } else {
            variablesToRemove = (Set<FastThreadLocal<?>>) v;
        }
        //往集合里面插入当前FastThreadLocal对象,0下标已经持有该集合的引用
        //所以先赋值集合到0下标,后插值到集合,是可以的
        variablesToRemove.add(variable);
    }

    public static void removeAll() {
        InternalThreadLocalMap threadLocalMap = InternalThreadLocalMap.getIfSet();
        if (threadLocalMap == null) {
            return;
        }

        try {
            Object v = threadLocalMap.indexedVariable(variablesToRemoveIndex);
            if (v != null && v != InternalThreadLocalMap.UNSET) {
                @SuppressWarnings("unchecked")
                Set<FastThreadLocal<?>> variablesToRemove = (Set<FastThreadLocal<?>>) v;
                FastThreadLocal<?>[] variablesToRemoveArray =
                        variablesToRemove.toArray(new FastThreadLocal[0]);
                for (FastThreadLocal<?> tlv: variablesToRemoveArray) {
                    tlv.remove(threadLocalMap);
                }
            }
        } finally {
            InternalThreadLocalMap.remove();
        }
    }
    

InternalThreadLocalMap.remove()

    public static void remove() {
        Thread thread = Thread.currentThread();
        if (thread instanceof FastThreadLocalThread) {
            ((FastThreadLocalThread) thread).setThreadLocalMap(null);
        } else {
            slowThreadLocalMap.remove();
        }
    }

最后我们可以看到在FastThreadLocalThread线程执行完run方法之后,一定会执行FastThreadLocal.removeAll();方法,最后解除相关引用,避免垃圾回收时还有强引用而回收失败导致内存泄漏的情况。

4.png

最后再来对比一下ThreadLocal

FastThreadLocal描述ThreadLocal描述
FastThreadLocalRunnable一个可执行任务Runnable一个可执行任务
InternalThreadLocalMap线程的成员变量ThreadLocalMap线程的成员变量
FastThreadLocalThread线程Thread线程

优缺点总结

FastThreadLocalThreadLocal快在哪:

  • 采用数组和下标获取的方式来进行数据的存取,获取数据以及存放数据都不需要计算hash值,时间复杂度O1

  • 因为FastThreadLocal对象在创建的时候就已经确定了index,所以后续的不管是get还是set或是remove都是基于确定的index去操作。时间复杂度都是O1。相当的快。

  • 还有就是在内存泄漏的处理上比ThreadLocal更优雅,ThreadLocal虽然把key设置为了弱引用,但是存在value回收不掉的情况,就造成了内存泄漏,当线程越来越多,堆积的未回收的value越多的时候就会导致内存溢出。FastThreadLocal就相当于没有key,硬要说有key就是index,然后value在线程执行完之后的finally代码块里面被清理掉。所以FastThreadLocal在内存泄漏的处理上不需要开发者做多余的考虑。虽然ThreadLocal会在set,get等方法里面将空key对应的value清理掉,但还是没有FastThreadLocal的设计来的智能。

总结: 所以说软件的设计是很重要的,同样的功能不同的设计带来的效率和结果是会不一样的。前期的设计很大程度上决定了最后产出软件的质量。然后说一下使用场景:首先我认为ThreadLocal能使用的场景FastThreadLocal都适用,其次可以使用在会产生大量ThreadLocal对象的系统,并且该系统对性能要求较高,因为当FastThreadLocal对象很多的时候获取某一个的value只需要用下标直接定位,速度非常快。或者对于系统稳定性要求比较高的系统,因为FastThreadLocal不需要开发者太关注内存泄漏的问题。

学而不思则罔,so 思考: 1.为什么不把Object[]对象直接放在FastThreadLocal中呢? 2.FastThreadLocal有什么缺点呢?

欢迎大家在评论区讨论~~~

5.png

微信公众号「 袋鼠先生的客栈 」,有问题评论区见。如果你觉得我的分享对你有帮助,或者觉得我有两把刷子,就支持一下我这个初出茅庐的writer吧,不需要money,三连,三连,三连就是我最大的动力~,接下来会持续分享一些干货~。点赞👍 关注❤️ 分享👥