乐趣区

关于java:深度揭秘Netty中的FastThreadLocal为什么比ThreadLocal效率更高

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

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 带你学架构
如果本篇文章对您有帮忙,还请帮忙点个关注和赞,您的保持是我一直创作的能源。欢送关注同名微信公众号获取更多技术干货!

退出移动版