浏览这篇文章之前,倡议先浏览和这篇文章关联的内容。

1. 具体分析散布式微服务架构下网络通信的底层实现原理(图解)

2. (年薪60W的技巧)工作了5年,你真的了解Netty以及为什么要用吗?(深度干货)

3. 深度解析Netty中的外围组件(图解+实例)

4. BAT面试必问细节:对于Netty中的ByteBuf详解

5. 通过大量实战案例合成Netty中是如何解决拆包黏包问题的?

6. 基于Netty实现自定义音讯通信协议(协定设计及解析利用实战)

7. 全网最具体最齐全的序列化技术及深度解析与利用实战

8. 手把手教你基于Netty实现一个根底的RPC框架(通俗易懂)

9. (年薪60W分水岭)基于Netty手写实现RPC框架进阶篇(带注册核心和注解)

FastThreadLocal的实现与J.U.C包中的ThreadLocal十分相似。

理解过ThreadLocal原理的同学应该都分明,它有几个要害的对象.

  1. Thread
  2. ThreadLocalMap
  3. ThreadLocal

同样,Netty专门为FastThreadLocal量身打造了FastThreadLocalThreadInternalThreadLocalMap两个重要的类。上面咱们看下这两个类是如何实现的。

PS,如果不懂ThreadLocal的敌人,能够看我这篇文章:ThreadLocal的应用及原理剖析

FastThreadLocalThread是对Thread类的一层包装,每个线程对应一个InternalThreadLocalMap实例。只有FastThreadLocalFastThreadLocalThread组合应用时,能力施展 FastThreadLocal的性能劣势。首先看下FastThreadLocalThread的源码定义:

public class FastThreadLocalThread extends Thread {    private InternalThreadLocalMap threadLocalMap;    // 省略其余代码}

能够看出 FastThreadLocalThread 次要扩大了 InternalThreadLocalMap 字段,咱们能够猜测到 FastThreadLocalThread 次要应用 InternalThreadLocalMap 存储数据,而不再是应用 Thread 中的 ThreadLocalMap。所以想晓得 FastThreadLocalThread 高性能的神秘,必须要理解 InternalThreadLocalMap 的设计原理。

InternalThreadLocalMap

public final class InternalThreadLocalMap extends UnpaddedInternalThreadLocalMap {    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;    private InternalThreadLocalMap() {        indexedVariables = newIndexedVariableTable();    }    private static Object[] newIndexedVariableTable() {        Object[] array = new Object[INDEXED_VARIABLE_TABLE_INITIAL_SIZE];        Arrays.fill(array, UNSET);        return array;    }    public static int lastVariableIndex() {        return nextIndex.get() - 1;    }    public static int nextVariableIndex() {        int index = nextIndex.getAndIncrement();        if (index < 0) {            nextIndex.decrementAndGet();            throw new IllegalStateException("too many thread-local indexed variables");        }        return index;    }    // 省略}

从 InternalThreadLocalMap 外部实现来看,与 ThreadLocalMap 一样都是采纳数组的存储形式。

理解ThreadLocal的同学都晓得,它外部也是采纳数组的形式来实现hash表,对于hash抵触,采纳了线性摸索的形式来实现。

然而 InternalThreadLocalMap 并没有应用线性探测法来解决 Hash 抵触,而是在 FastThreadLocal 初始化的时候调配一个数组索引 index,index 的值采纳原子类 AtomicInteger 保障程序递增,通过调用 InternalThreadLocalMap.nextVariableIndex() 办法取得。而后在读写数据的时候通过数组下标 index 间接定位到 FastThreadLocal 的地位,工夫复杂度为 O(1)。如果数组下标递增到十分大,那么数组也会比拟大,所以 FastThreadLocal 是通过空间换工夫的思维晋升读写性能。

上面通过一幅图形容 InternalThreadLocalMap、index 和 FastThreadLocal 之间的关系。

通过下面 FastThreadLocal 的外部结构图,咱们比照下与 ThreadLocal 有哪些区别呢?

FastThreadLocal 应用 Object 数组代替了 Entry 数组,Object[0] 存储的是一个Set<FastThreadLocal<?>> 汇合。

从数组下标 1 开始都是间接存储的 value 数据,不再采纳 ThreadLocal 的键值对模式进行存储。

假如当初咱们有一批数据须要增加到数组中,别离为 value1、value2、value3、value4,对应的 FastThreadLocal 在初始化的时候生成的数组索引别离为 1、2、3、4。如下图所示。

至此,咱们曾经对 FastThreadLocal 有了一个根本的意识,上面咱们联合具体的源码剖析 FastThreadLocal 的实现原理。

FastThreadLocal的set办法源码剖析

在解说源码之前,咱们回过头看下上文中的 ThreadLocal 示例,如果把示例中 ThreadLocal 替换成 FastThread,该当如何应用呢?

public class FastThreadLocalTest {    private static final FastThreadLocal<String> THREAD_NAME_LOCAL = new FastThreadLocal<>();    private static final FastThreadLocal<TradeOrder> TRADE_THREAD_LOCAL = new FastThreadLocal<>();    public static void main(String[] args) {        for (int i = 0; i < 2; i++) {            int tradeId = i;            String threadName = "thread-" + i;            new FastThreadLocalThread(() -> {                THREAD_NAME_LOCAL.set(threadName);                TradeOrder tradeOrder = new TradeOrder(tradeId, tradeId % 2 == 0 ? "已领取" : "未领取");                TRADE_THREAD_LOCAL.set(tradeOrder);                System.out.println("threadName: " + THREAD_NAME_LOCAL.get());                System.out.println("tradeOrder info:" + TRADE_THREAD_LOCAL.get());            }, threadName).start();        }    }}

能够看出,FastThreadLocal 的应用办法简直和 ThreadLocal 保持一致,只须要把代码中 Thread、ThreadLocal 替换为 FastThreadLocalThread 和 FastThreadLocal 即可,Netty 在易用性方面做得相当棒。上面咱们重点对示例中用失去 FastThreadLocal.set()/get() 办法做深入分析。

首先看下 FastThreadLocal.set() 的源码:
public final void set(V value) {    if (value != InternalThreadLocalMap.UNSET) {        InternalThreadLocalMap threadLocalMap = InternalThreadLocalMap.get();        setKnownNotUnset(threadLocalMap, value);    } else {        remove();    }}

FastThreadLocal.set() 办法实现并不难理解,先抓住代码骨干,一步步进行拆解剖析。set() 的过程次要分为三步:

  1. 判断 value 是否为缺省值,如果等于缺省值,那么间接调用 remove() 办法。这里咱们还不晓得缺省值和 remove() 之间的分割是什么,咱们暂且把 remove() 放在最初剖析。
  2. 如果 value 不等于缺省值,接下来会获取以后线程的 InternalThreadLocalMap。
  3. 而后将 InternalThreadLocalMap 中对应数据替换为新的 value。

InternalThreadLocalMap.get()

先来看InternalThreadLocalMap.get()办法:

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

如果thread实例类型是FastThreadLocalThread,则调用fastGet()。

InternalThreadLocalMap.get() 逻辑很简略.

  1. 如果以后线程是 FastThreadLocalThread 类型,那么间接通过 fastGet() 办法获取 FastThreadLocalThread 的 threadLocalMap 属性即可
  2. 如果此时 InternalThreadLocalMap 不存在,间接创立一个返回。
对于 InternalThreadLocalMap 的初始化在上文中曾经介绍过,它会初始化一个长度为 32 的 Object 数组,数组中填充着 32 个缺省对象 UNSET 的援用。
private static InternalThreadLocalMap fastGet(FastThreadLocalThread thread) {  InternalThreadLocalMap threadLocalMap = thread.threadLocalMap();  if (threadLocalMap == null) {    thread.setThreadLocalMap(threadLocalMap = new InternalThreadLocalMap());  }  return threadLocalMap;}

否则,则调用slowGet(),从代码实现来看,slowGet() 是针对非 FastThreadLocalThread 类型的线程发动调用时的一种兜底计划。如果以后线程不是 FastThreadLocalThread,外部是没有 InternalThreadLocalMap 属性的,Netty 在 UnpaddedInternalThreadLocalMap 中保留了一个 JDK 原生的 ThreadLocal,ThreadLocal 中寄存着 InternalThreadLocalMap,此时获取 InternalThreadLocalMap 就进化成 JDK 原生的 ThreadLocal 获取。

private static InternalThreadLocalMap slowGet() {  InternalThreadLocalMap ret = slowThreadLocalMap.get();  if (ret == null) {    ret = new InternalThreadLocalMap();    slowThreadLocalMap.set(ret);  }  return ret;}

setKnownNotUnset

获取 InternalThreadLocalMap 的过程曾经讲完了,上面看下 setKnownNotUnset() 如何将数据增加到 InternalThreadLocalMap 的。

private void setKnownNotUnset(InternalThreadLocalMap threadLocalMap, V value) {    if (threadLocalMap.setIndexedVariable(index, value)) {        addToVariablesToRemove(threadLocalMap, this);    }}

setKnownNotUnset() 次要做了两件事:

  1. 找到数组下标 index 地位,设置新的 value。
  2. 将 FastThreadLocal 对象保留到待清理的 Set 中。

首先咱们看下第一步 threadLocalMap.setIndexedVariable() 的源码实现:

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

indexedVariables 就是 InternalThreadLocalMap 中用于存放数据的数组,如果数组容量大于 FastThreadLocal 的 index 索引,那么间接找到数组下标 index 地位将新 value 设置进去,事件复杂度为 O(1)。在设置新的 value 之前,会将之前 index 地位的元素取出,如果旧的元素还是 UNSET 缺省对象,那么返回胜利。

如果数组容量不够了怎么办呢?InternalThreadLocalMap 会主动扩容,而后再设置 value。接下来看看 expandIndexedVariableTableAndSet() 的扩容逻辑:

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

能够看出 InternalThreadLocalMap 实现数组扩容简直和 HashMap 齐全是截然不同的,所以多读源码还是能够给咱们很多启发的。InternalThreadLocalMap 以 index 为基准进行扩容,将数组扩容后的容量向上取整为 2 的次幂。而后将原数组内容拷贝到新的数组中,空余局部填充缺省对象 UNSET,最终把新数组赋值给 indexedVariables。

思考对于基准扩容

思考:为什么 InternalThreadLocalMap 以 index 为基准进行扩容,而不是原数组长度呢?

假如当初初始化了 70 个 FastThreadLocal,然而这些 FastThreadLocal 素来没有调用过 set() 办法,此时数组还是默认长度 32。当第 index = 70 的 FastThreadLocal 调用 set() 办法时,如果按原数组容量 32 进行扩容 2 倍后,还是无奈填充 index = 70 的数据。所以应用 index 为基准进行扩容能够解决这个问题,然而如果 FastThreadLocal 特地多,数组的长度也是十分大的。

回到 setKnownNotUnset() 的主流程,向 InternalThreadLocalMap 增加完数据之后,接下就是将 FastThreadLocal 对象保留到待清理的 Set 中。咱们持续看下 addToVariablesToRemove() 是如何实现的:

addToVariablesToRemove

private static void addToVariablesToRemove(InternalThreadLocalMap threadLocalMap, FastThreadLocal<?> variable) {    Object v = threadLocalMap.indexedVariable(variablesToRemoveIndex);    Set<FastThreadLocal<?>> variablesToRemove;    if (v == InternalThreadLocalMap.UNSET || v == null) {        variablesToRemove = Collections.newSetFromMap(new IdentityHashMap<FastThreadLocal<?>, Boolean>());        threadLocalMap.setIndexedVariable(variablesToRemoveIndex, variablesToRemove);    } else {        variablesToRemove = (Set<FastThreadLocal<?>>) v;    }    variablesToRemove.add(variable);}

variablesToRemoveIndex 是采纳 static final 润饰的变量,在 FastThreadLocal 初始化时 variablesToRemoveIndex 被赋值为 0。InternalThreadLocalMap 首先会找到数组下标为 0 的元素.

  1. 如果该元素是缺省对象 UNSET 或者不存在,那么会创立一个 FastThreadLocal 类型的 Set 汇合,而后把 Set 汇合填充到数组下标 0 的地位。
  2. 如果数组第一个元素不是缺省对象 UNSET,阐明 Set 汇合曾经被填充,间接强转取得 Set 汇合即可。这就解释了 InternalThreadLocalMap 的 value 数据为什么是从下标为 1 的地位开始存储了,因为 0 的地位曾经被 Set 汇合占用了。

思考对于Set汇合设计

思考:为什么 InternalThreadLocalMap 要在数组下标为 0 的地位寄存一个 FastThreadLocal 类型的 Set 汇合呢?这时候咱们回过头看下 remove() 办法。
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);    }  }}

在执行 remove 操作之前,会调用 InternalThreadLocalMap.getIfSet() 获取以后 InternalThreadLocalMap。

有了之前的根底,了解 getIfSet() 办法就非常简单了。

  1. 如果是 FastThreadLocalThread 类型,间接取 FastThreadLocalThread 中 threadLocalMap 属性。
  2. 如果是一般线程 Thread,从 ThreadLocal 类型的 slowThreadLocalMap 中获取。

找到 InternalThreadLocalMap 之后,InternalThreadLocalMap 会从数组中定位到下标 index 地位的元素,并将 index 地位的元素笼罩为缺省对象 UNSET。

接下来就须要清理以后的 FastThreadLocal 对象,此时 Set 汇合就派上了用场,InternalThreadLocalMap 会取出数组下标 0 地位的 Set 汇合,而后删除以后 FastThreadLocal。最初 onRemoval() 办法起到什么作用呢?Netty 只是留了一处扩大,并没有实现,用户须要在删除的时候做一些后置操作,能够继承 FastThreadLocal 实现该办法。

FastThreadLocal.get()源码剖析

再来看一下 FastThreadLocal.get() 的源码:
public final V get() {    InternalThreadLocalMap threadLocalMap = InternalThreadLocalMap.get();    Object v = threadLocalMap.indexedVariable(index);    if (v != InternalThreadLocalMap.UNSET) {        return (V) v;    }    return initialize(threadLocalMap);}

首先依据以后线程是否是 FastThreadLocalThread 类型找到 InternalThreadLocalMap,而后取出从数组下标 index 的元素,如果 index 地位的元素不是缺省对象 UNSET,阐明该地位曾经填充过数据,间接取出返回即可。

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

如果 index 地位的元素是缺省对象 UNSET,那么须要执行初始化操作。能够看到,initialize() 办法会调用用户重写的 initialValue 办法结构须要存储的对象数据.

private V initialize(InternalThreadLocalMap threadLocalMap) {    V v = null;    try {        v = initialValue();    } catch (Exception e) {        PlatformDependent.throwException(e);    }    threadLocalMap.setIndexedVariable(index, v);    addToVariablesToRemove(threadLocalMap, this);    return v;}
initialValue办法的结构形式如下。
private final FastThreadLocal<String> threadLocal = new FastThreadLocal<String>() {  @Override  protected String initialValue() {    return "hello world";  }};

结构完用户对象数据之后,接下来就会将它填充到数组 index 的地位,而后再把以后 FastThreadLocal 对象保留到待清理的 Set 中。整个过程咱们在剖析 FastThreadLocal.set() 时都曾经介绍过,就不再赘述了。

到此为止,FastThreadLocal 最外围的两个办法 set()/get() 咱们曾经剖析完了。上面有两个问题咱们再深刻思考下。

  1. FastThreadLocal 真的肯定比 ThreadLocal 快吗?答案是不肯定的,只有应用FastThreadLocalThread 类型的线程才会更快,如果是一般线程反而会更慢。
  2. FastThreadLocal 会节约很大的空间吗?尽管 FastThreadLocal 采纳的空间换工夫的思路,然而在 FastThreadLocal 设计之初就认为不会存在特地多的 FastThreadLocal 对象,而且在数据中没有应用的元素只是寄存了同一个缺省对象的援用,并不会占用太多内存空间。
版权申明:本博客所有文章除特地申明外,均采纳 CC BY-NC-SA 4.0 许可协定。转载请注明来自 Mic带你学架构
如果本篇文章对您有帮忙,还请帮忙点个关注和赞,您的保持是我一直创作的能源。欢送关注同名微信公众号获取更多技术干货!