Netty中的FastThreadLocal有多快?終于找到快的原因

解決的問題?

在多線程環(huán)境下訪問共享變量?大家都能想到的是通過加鎖串行化處理可以解決,但是在高并發(fā)的場景下,加鎖操作是不是就存在瓶頸了?

  • JDK 自帶的 ThreadLocal 的出現(xiàn),可以保證多線程下無鎖化的線程安全;可以發(fā)現(xiàn)很多開源框架中大量使用了 ThreadLocal 解決這一類問題

  • FastThreadLocal 為什么出現(xiàn)?離開了具體場景談技術的應用難免有些不妥,在好的東西也有它最適用的范圍;基于 Netty 的應用場景,演化出了更適用于它的 FastThreadLocal 來訪問控制共享變量。


一、對比JDK自帶的ThreadLocal

對于 ThreadLocal 這種線程間數(shù)據(jù)隔離方案,真實的數(shù)據(jù)維護其實是由線程自己維護并引用;比如 JDK 的 ThreadLocal 實現(xiàn)方案是由線程內(nèi)部維護了一張 Map 結構存儲這些數(shù)據(jù),用的時候直接取就可以

  • JDK自帶的 ThreadLocal,Thread 內(nèi)部維護了 ThreadLocal.ThreadLocalMap 這樣一個引用關系,使用的 hash + 線性探測解決沖突的一套方案;這套方案在大多場景下性能都是很 OK的,畢竟一個 Thread 內(nèi)部也不會存在很多 ThreadLocal 對象,換句話說,存在 hash 沖突的情況就很小,那讀寫操作都是O(1)自然性能很好,又能回收并復用過期的 hash 槽,空間效率也很OK。

  • 但對于一個線程內(nèi)部可能出現(xiàn)很多的 ThreadLocal 對象的場景,JDK 那套 hash 沖突的解決方案可能就不那么美妙了。

  • 對于 Netty 這套網(wǎng)絡通信框架而言,即有底層通信層的封裝、也有編/解碼的處理、更很多業(yè)務層的邏輯,可能存在一個線程中維護很多 threadLocal 對象的場景,因此 netty 從 JDK 的 TreadLocal 中衍生出適合自己業(yè)務的 FastThreadLocal。

  • 主要的改進場景在于:線程維護的結構由 Map 變成了 Array 數(shù)組結構,最大的好處是可以 O(1) 讀寫,但也存在數(shù)據(jù)很大的場景;典型的空間換時間思想的應用。

二、FastThreadLocal 為什么快?

2.1 重要結構

FastThreadLocal 的實現(xiàn)與 ThreadLocal 非常類似,Netty 為 FastThreadLocal 量身打造了 FastThreadLocalThread 和 InternalThreadLocalMap 兩個重要的類:

  • FastThreadLocalThread 是對 Thread 類做了一層擴展,也就是通過擴展的 InternalThreadLocalMap 結構維護自己的一套數(shù)據(jù)。
  • 只有 FastThreadLocalFastThreadLocalThread 組合使用時,才能發(fā)揮 FastThreadLocal 的性能優(yōu)勢。因為不使用FastThreadLocalThread 就沒有擴展結構存儲數(shù)據(jù)。
public class FastThreadLocalThread extends Thread {

    private InternalThreadLocalMap threadLocalMap;

    // 省略其他代碼

}
  • 有了自己擴展的結構 InternalThreadLocalMap,就可以不再使用 Thread 中的 ThreadLocalMap。所以想知道 FastThreadLocalThread 高性能的奧秘,必須要了解 InternalThreadLocalMap 的設計原理。

2.2 提升的關鍵

前面講到 ThreadLocal 的一個重要缺點,就是 ThreadLocalMap 采用線性探測法解決 Hash 沖突性能較慢,那么 InternalThreadLocalMap 又是如何優(yōu)化的呢?

先來看看 InternalThreadLocalMap 的內(nèi)部構造:

public final class InternalThreadLocalMap extends UnpaddedInternalThreadLocalMap {

    private static final InternalLogger logger = InternalLoggerFactory.getInstance(InternalThreadLocalMap.class);

    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;

    // ...

    public static InternalThreadLocalMap get() {
        Thread thread = Thread.currentThread();
        if (thread instanceof FastThreadLocalThread) {
            return fastGet((FastThreadLocalThread) thread);
        } else {
            // 兜底操作, 如果不是FastThreadLocalThread,就使用JDK的ThreadLocal處理
            return slowGet();
        }
    }

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

    // 通過線程內(nèi)部的AtomicInteger原子性遞增的獲取數(shù)據(jù)下標
    // 隨著逐步遞增,下標也越來越大,也就是數(shù)組越來越大,這是它最大的缺點之一
    public static int nextVariableIndex() {
        int index = nextIndex.getAndIncrement();
        if (index < 0) {
            nextIndex.decrementAndGet();
            throw new IllegalStateException("too many thread-local indexed variables");
        }
        return index;
    }

    // ...

在 FastThreadLocal 初始化的時候分配一個數(shù)組索引 index,index 的值采用原子類 AtomicInteger 保證順序遞增,通過調(diào)用 InternalThreadLocalMap.nextVariableIndex() 方法獲得。

然后在讀寫數(shù)據(jù)的時候通過數(shù)組下標 index 直接定位到 FastThreadLocal 的位置,時間復雜度為 O(1)。如果數(shù)組下標遞增到非常大,那么數(shù)組也會比較大,所以 FastThreadLocal 是通過空間換時間的思想提升讀寫性能。

InternalThreadLocalMa p數(shù)組下標 0 存的非實質性數(shù)據(jù),是一個Set<FastThreadLocal<?>>類型,也就是所有的 FastThreadLocal 引用集合,用于擴展 remove 操作

從數(shù)組下標 1 開始都是直接存儲的 value 數(shù)據(jù),不再采用 ThreadLocal 的鍵值對形式進行存儲。

InternalThreadLocalMap、index 和 FastThreadLocal 之間的關系



三、FastThreadLocal 源碼分析

3.1 使用差異

從基本的使用上來看和 ThreadLocal 基本沒有差異,只需要把代碼中 Thread、ThreadLocal 替換為 FastThreadLocalThread 和 FastThreadLocal 即可。

下面我們重點對示例中用得到 FastThreadLocal.set()/get() 方法做深入分析

3.2 重點實現(xiàn)

3.2.1 FastThreadLocal.set()

    /**
     * Set the value for the current thread.
     */
    public final void set(V value) {
        if (value != InternalThreadLocalMap.UNSET) {
            InternalThreadLocalMap threadLocalMap = InternalThreadLocalMap.get();
            setKnownNotUnset(threadLocalMap, value);
        } else {
            remove();
        }
    }
  • 判斷 value 是否為缺省值,如果等于缺省值,那么直接調(diào)用 remove() 方法。

  • 如果 value 不等于缺省值,接下來會獲取當前線程的 InternalThreadLocalMap。

  • 然后將 InternalThreadLocalMap 中對應數(shù)據(jù)替換為新的 value。

3.2.2 InternalThreadLocalMap.get()

    public static InternalThreadLocalMap get() {
        Thread thread = Thread.currentThread();
        // 如果是FastThreadLocalThread類型,則從FastThreadLocalThread中拿去
        if (thread instanceof FastThreadLocalThread) {
            return fastGet((FastThreadLocalThread) thread);
        } else {
            // 兜底操作
            // 從JDK的ThreadLocal中獲取
            return slowGet();
        }
    }

    private static InternalThreadLocalMap fastGet(FastThreadLocalThread thread) {
        InternalThreadLocalMap threadLocalMap = thread.threadLocalMap();
        // 如果此時 InternalThreadLocalMap 不存在,直接創(chuàng)建一個返回
        if (threadLocalMap == null) {
            thread.setThreadLocalMap(threadLocalMap = new InternalThreadLocalMap());
        }
        return threadLocalMap;
    }

    private static InternalThreadLocalMap slowGet() {
        ThreadLocal<InternalThreadLocalMap> slowThreadLocalMap = UnpaddedInternalThreadLocalMap.slowThreadLocalMap;
        // 從JDK的ThreadLocal中獲取InternalThreadLocalMap對象
        InternalThreadLocalMap ret = slowThreadLocalMap.get();
        if (ret == null) {
            ret = new InternalThreadLocalMap();
            slowThreadLocalMap.set(ret);
        }
        return ret;
    }
  • InternalThreadLocalMap 初始化,它會初始化一個長度為 32 的 Object 數(shù)組,數(shù)組中填充著 32 個缺省對象 UNSET 的引用
  • slowGet() 是針對非 FastThreadLocalThread 類型的線程發(fā)起調(diào)用時的一種兜底方案。如果當前線程不是 FastThreadLocalThread,內(nèi)部是沒有 InternalThreadLocalMap 屬性的,Netty 在 UnpaddedInternalThreadLocalMap 中保存了一個 JDK 原生的 ThreadLocal,ThreadLocal 中存放著 InternalThreadLocalMap,此時獲取 InternalThreadLocalMap 就退化成 JDK 原生的 ThreadLocal 獲取

3.2.3 setKnownNotUnset()

    /**
     * @return see {@link InternalThreadLocalMap#setIndexedVariable(int, Object)}.
     */
    private void setKnownNotUnset(InternalThreadLocalMap threadLocalMap, V value) { 
        // 給指定的下標賦值,如果存在舊值,則直接覆蓋
        if (threadLocalMap.setIndexedVariable(index, value)) {
            // 將 FastThreadLocal 對象保存到待清理的 Set 中
            addToVariablesToRemove(threadLocalMap, this);
        }
    }

3.2.4 threadLocalMap.setIndexedVariable()

    /**
     * @return {@code true} if and only if a new thread-local variable has been created
     */
    public boolean setIndexedVariable(int index, Object value) {
        // indexedVariables就是InternalThreadLocalMap中用于存放數(shù)據(jù)的數(shù)組
        Object[] lookup = indexedVariables;
        if (index < lookup.length) {
            Object oldValue = lookup[index];
            // 直接將數(shù)組 index 位置設置為 value,時間復雜度為 O(1)
            lookup[index] = value;
            return oldValue == UNSET;
        } else {
            // 如果容量不夠,先擴容,再新增
            expandIndexedVariableTableAndSet(index, value);
            return true;
        }
    }

3.2.5 addToVariablesToRemove()

將 FastThreadLocal 對象保存到待清理的 Set 中

    private static void addToVariablesToRemove(InternalThreadLocalMap threadLocalMap, FastThreadLocal<?> variable) {
        // variablesToRemoveIndex, 數(shù)組下標 固定值0
        Object v = threadLocalMap.indexedVariable(variablesToRemoveIndex);
        Set<FastThreadLocal<?>> variablesToRemove;
        // 邏輯是,要拿到Set<FastThreadLocal<?>>集合,如果沒有就創(chuàng)建并塞進indexedVariable[0]中
        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);
    }

這里就解釋了 InternalThreadLocalMap 的 value 數(shù)據(jù)為什么是從下標為 1 的位置開始存儲了,因為 0 的位置已經(jīng)被 Set 集合占用了

為什么 InternalThreadLocalMap 要在數(shù)組下標為 0 的位置存放一個 FastThreadLocal 類型的 Set 集合呢?

    /**
     * Sets the value to uninitialized; a proceeding call to get() will trigger a call to initialValue().
     */
    public final void remove() {
        remove(InternalThreadLocalMap.getIfSet());
    }

    public static InternalThreadLocalMap getIfSet() {
        Thread thread = Thread.currentThread();
        if (thread instanceof FastThreadLocalThread) {
            return ((FastThreadLocalThread) thread).threadLocalMap();
        }
        return slowThreadLocalMap.get();
    }

    /**
     * Sets the value to uninitialized for the specified thread local map;
     * a proceeding call to get() will trigger a call to initialValue().
     * The specified thread local map must be for the current thread.
     */
    public final void remove(InternalThreadLocalMap threadLocalMap) {
        if (threadLocalMap == null) {
            return;
        }
        // index是當前FastThreadLocal對應在數(shù)組中的下標位置
        // 將下標fastThreadLocal對應value置為UNSET
        Object v = threadLocalMap.removeIndexedVariable(index);
        // 刪除當前FastThreadLocal對象
        removeFromVariablesToRemove(threadLocalMap, this);

        if (v != InternalThreadLocalMap.UNSET) {
            try {
                // 空操作,子類可擴展
                onRemoval((V) v);
            } catch (Exception e) {
                PlatformDependent.throwException(e);
            }
        }
    }

因此Set 集合是為了保存 FastThreadLocal對象,好處有幾點

  • 刪除 FastThreadLocal 留擴展接口。
  • 提高 removeAll 的刪除效率,不需要去遍歷膨脹的數(shù)組。
  • 可以更好地做內(nèi)存泄露的管理

3.2.6 get()操作

   public final V get(InternalThreadLocalMap threadLocalMap) {
        Object v = threadLocalMap.indexedVariable(index);
        if (v != InternalThreadLocalMap.UNSET) {
            return (V) v;
        }
        // 如果獲取到的數(shù)組元素是缺省對象,執(zhí)行初始化操作
        return initialize(threadLocalMap);
    }

    private V initialize(InternalThreadLocalMap threadLocalMap) {
        V v = null;
        try {
            // 擴展實現(xiàn), 用戶可實現(xiàn)初始化操作
            v = initialValue();
        } catch (Exception e) {
            PlatformDependent.throwException(e);
        }

        threadLocalMap.setIndexedVariable(index, v);
        addToVariablesToRemove(threadLocalMap, this);
        return v;
    }
  • 如果能拿到對象,就直接返回
  • 拿不到就通過initialize初始化對象
  • 構造完用戶對象數(shù)據(jù)之后,接下來就會將它填充到數(shù)組 index 的位置,然后再把當前 FastThreadLocal 對象保存到待清理的 Set 中

總結

對比ThreadLocal 和 FastThreadLocal,簡單總結下 FastThreadLocal 的優(yōu)勢。

  • 高效查找,F(xiàn)astThreadLocal 通過 index 直接定位在數(shù)組中的位置,O(1) 操作;ThreadLocal 內(nèi)部使用 hash+線性探測,有可能出現(xiàn)沖突,要往后線性查找合適的位置。
  • 安全性更高,JDK 原生的 ThreadLocal 使用不當可能造成內(nèi)存泄漏。而 FastThreadLocal 不僅提供了 remove() 主動清除對象的方法,而且在線程池場景中 Netty 還封裝了 FastThreadLocalRunnable,F(xiàn)astThreadLocalRunnable 最后會執(zhí)行 FastThreadLocal.removeAll() 將 Set 集合中所有 FastThreadLocal 對象都清理掉。
  • 更高效的擴容,F(xiàn)astThreadLocal 相比 ThreadLocal 數(shù)據(jù)擴容更加簡單高效,F(xiàn)astThreadLocal 以 index 為基準向上取整到 2 的次冪作為擴容后容量,然后把原數(shù)據(jù)拷貝到新數(shù)組。而 ThreadLocal 由于采用的哈希表,所以在擴容后需要再做一輪 rehash。

缺點:

  • 數(shù)組 index 不會復用,會持續(xù)增長,空間消耗較大。
  • 需要結合 FastThreadLocalThread 使用,否則也會退化成 ThreadLocal 處理。
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容