netty系列之:給ThreadLocal插上夢想的翅膀,詳解FastThreadLocal

flydean發表於2022-06-06

簡介

JDK中的ThreadLocal可以通過get方法來獲得跟當前執行緒繫結的值。而這些值是儲存在ThreadLocal.ThreadLocalMap中的。而在ThreadLocalMap中底層的資料儲存是一個Entry陣列中的。

那麼從ThreadLocalMap中獲取資料的速度如何呢?速度有沒有可以優化的空間呢?

一起來看看。

從ThreadLocalMap中獲取資料

ThreadLocalMap作為一個Map,它的底層資料儲存是一個Entry型別的陣列:

private Entry[] table;

我們再來回顧一下ThreadLocal是怎麼獲取資料的:

        private Entry getEntry(ThreadLocal<?> key) {
            int i = key.threadLocalHashCode & (table.length - 1);
            Entry e = table[i];
            if (e != null && e.get() == key)
                return e;
            else
                return getEntryAfterMiss(key, i, e);
        }

首先根據ThreadLocal物件中的threadLocalHashCode跟table的長度進行取模運算,得到要獲取的Entry在table中的位置,然後判斷位置Entry的key是否和要獲取的ThreadLocal物件一致。

如果一致,說明獲取到了ThreadLocal繫結的物件,直接返回即可。

如果不一致,則需要再次進行查詢。

我們看下再次查詢的邏輯:

        private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
            Entry[] tab = table;
            int len = tab.length;

            while (e != null) {
                ThreadLocal<?> k = e.get();
                if (k == key)
                    return e;
                if (k == null)
                    expungeStaleEntry(i);
                else
                    i = nextIndex(i, len);
                e = tab[i];
            }
            return null;
        }

getEntryAfterMiss的邏輯是,先判斷Entry中的物件是否要獲取的物件,如果是則直接返回。

如果Entry中的物件為空,則觸發清除過期Entry的方法。否則的話計算出下一個要判斷的地址,再次進行判斷,直到最終找到要找到的物件為止。

可以看到,如果第一次沒有找到要找到的物件的話,後面則可能會遍歷多次,從而造成執行效率變低。

那麼有沒有可以提升這個尋找速度的方法呢?答案是肯定的。

FastThreadLocal

之前我們提到了,Netty中的本地物件池技術,netty為其建立了一個專門的類叫做Recycler。雖然Recycler中也使用到了ThreadLocal,但是Recycler使用的threadLocal並不是JDK自帶的ThreadLocal,而是FastThreadLocal。和它關聯的ThreadLocalMap叫做InternalThreadLocalMap,和它關聯的Thread叫做FastThreadLocalThread。netty中的類和JDK中的類的對應關係如下:

netty中的物件JDK中的物件
FastThreadLocalThreadThread
InternalThreadLocalMapThreadLocal.ThreadLocalMap
FastThreadLocalThreadLocal

我們先來看FastThreadLocalThread。不管它到底快不快,既然是Thread,那麼自然就要繼承自JDK的Thread:

public class FastThreadLocalThread extends Thread

和Thread一樣,FastThreadLocalThread中也有一個ThreadLocalMap,叫做InternalThreadLocalMap,它是FastThreadLocalThread的private屬性:

private InternalThreadLocalMap threadLocalMap;

InternalThreadLocalMap中也有一個ThreadLocal物件,叫做slowThreadLocalMap,是在fastThreadLocalMap不生效的時候使用的。

接下來我們來看下這個ThreadLocalMap為什麼快:

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

從get方法可以看到,如果當前thread是FastThreadLocalThread的話,則會去呼叫fastGet方法,否則呼叫slowGet方法。

slowGet方法就是使用傳統的ThreadLocal來get:

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

我們重點關注下fastGet方法:

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

這裡fast的效果就出現了,fastGet直接返回了thread中的InternalThreadLocalMap物件,不需要進行任何查詢的過程。

再看下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);
    }

可以看到FastThreadLocal中的get首先呼叫了InternalThreadLocalMap的get方法,直接返回了FastThreadLocalThread中的InternalThreadLocalMap物件,這個速度是非常快的。

然後直接使用FastThreadLocal中的index,來獲取threadLocalMap中具體儲存資料的陣列中的元素:

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

因為是直接index訪問的,所以也非常快。這就是fast的由來。

那麼有同學會問題了,FastThreadLocal中的index是怎麼來的呢?

    private final int index;

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

而InternalThreadLocalMap中的nextVariableIndex方法是一個靜態方法:

    public static int nextVariableIndex() {
        int index = nextIndex.getAndIncrement();
        if (index < 0) {
            nextIndex.decrementAndGet();
            throw new IllegalStateException("too many thread-local indexed variables");
        }
        return index;
    }

也就是說,只要new一個FastThreadLocal,該物件中,就會生成一個唯一的index。然後FastThreadLocal使用該index去InternalThreadLocalMap中存取物件。這樣就不存在ThreadLocal那種需要多次遍歷查詢的情況。

總結

FastThreadLocal是和FastThreadLocalThread配套使用才會真正的fast,否則的話就會fallback到ThreadLocal去執行,大家一定要注意這一點。

更多內容請參考 http://www.flydean.com/48-netty-fastthreadlocal/

最通俗的解讀,最深刻的乾貨,最簡潔的教程,眾多你不知道的小技巧等你來發現!

歡迎關注我的公眾號:「程式那些事」,懂技術,更懂你!

相關文章