Netty精粹之設計更快的ThreadLocal

Netty是一款優秀的開源的NIO框架,其異步的、基於IO事件驅動的設計以及簡易使用的API使得用戶快速構建基於NIO的高性能高可靠性的網絡服務器成爲可能。Netty除了使用Reactor設計模式加上精心設計的線程模型以外,對於線程建立的具體細節也進行了從新設計,因爲Netty的應用場景主要面向高併發高負載的場景下,這也是Netty可以大顯身手的場景,所以,Netty不放過任何優化性能的機會。這篇文章主要介紹Netty線程模型基礎部分——線程建立相關以及FastThreadLocal實現方面的一些細節以及和傳統的ThreadLocal之間的性能比較數據。java

傳統的ThreadLocal設計模式

ThreadLocal最經常使用的兩個接口是set和get,前者是用於往ThreadLocal設置內容,後者是從ThreadLocal中取內容。最多見的應用場景爲在線程上下文之間傳遞信息,使得用戶不受複雜代碼邏輯的影響。咱們來看看他們的實現原理:數組

public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
 t.threadLocals;

咱們使用set的時候其實是獲取Thread對象的threadLocals屬性,把當前ThreadLocal當作參數而後調用其set(ThreadLocal,Object)方法來設值。threadLocals是ThreadLocal.ThreadLocalMap類型的。所以咱們能夠知道Thread、ThreadLoca以及ThreadLocal.ThreadLocalMap的關係能夠用下圖表示:安全

解釋一下上面的圖,每一個線程對象關聯着一個ThreadLocalMap實例,ThreadLocalMap實例主要是維護着一個Entry數組。Entry是擴展了WeakReference,提供了一個存儲value的地方。一個線程對象能夠對應多個ThreadLocal實例,一個ThreadLocal也能夠對應多個Thread對象,當一個Thread對象和每個ThreadLocal發生關係的時候會生成一個Entry,並將須要存儲的值存儲在Entry的value內。到這裏咱們能夠總結一下幾點:服務器

  1. 一個ThreadLocal對於一個Thread對象來講只能存儲一個值,爲Object類型。網絡

  2. 多個ThreadLocal對於一個Thread對象,這些ThreadLocal和線程相關的值存儲在Thread對象關聯的ThreadLocalMap中。併發

  3. 使用擴展WeakReference的Entry做爲數據節點在必定程度上防止了內存泄露。框架

  4. 多個Thread線程對象和一個ThreadLocal發生關係的時候其實真是數據的存儲是跟着線程對象走的,所以這種狀況不討論。
    異步

咱們在看看ThreadLocalMap#set:ide

Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);

for (Entry e = tab[i];
     e != null;
     e = tab[i = nextIndex(i, len)]) {
    ThreadLocal k = e.get();
    if (k == key) {
        e.value = value;
        return;
    }
    if (k == null) {
        replaceStaleEntry(key, value, i);
        return;
    }
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
    rehash();


通常狀況每一個ThreadLocal實例都有一個惟一的threadLocalHashCode初始值。上面首先根據threadLocalHashCode值計算出i,有下面兩種狀況會進入for循環:

  1. 因爲threadLocalHashCode&(len-1)的值對應的槽有內容,所以知足tab[i]!=null條件,進入for循環,若是知足條件且當前key不是當前threadlocal只能說明hash衝突了。

  2. ThreadLocal實例以前被設值過,所以足tab[i]!=null條件,進入for循環。

進入for循環會遍歷tab數組,若是遇到以當前threadLocal爲key的槽,即上面第(2)種狀況,有則直接將值替換;若是找到了一個已經被回收的ThreadLocal對應的槽,也就是當key==null的時候表示以前的threadlocal已經被回收了,可是value值還存在,這也是ThreadLocal內存泄露的地方。碰到這種狀況,則會引起替換這個位置的動做,若是上面兩種狀況都沒發生,即上面的第(1)種狀況,則新建立一個Entry對象放入槽中。

看看ThreadLocalMap的讀取實現:

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計算出來的i剛好是當前ThreadLocal設置的值的時候,能夠直接根據hashcode來計算出位置,當沒有命中的時候,這裏沒有命中分爲三種狀況:

  1. 當前ThreadLocal以前沒有設值過,而且當前槽位沒有值。

  2. 當前槽位有值,可是對於的不是當前threadlocal,且那個ThreadLocal沒有被回收。

  3. 當前槽位有值,可是對於的不是當前threadlocal,且那個ThreadLocal被回收了。

上面三種狀況都會調用getEntryAfterMiss方法。調用getEntryAfterMiss方法會引起數組的遍歷。


總結一下ThreadLocal的性能,一個線程對應多個ThreadLocal實例的場景中,在沒有命中的狀況下基本上一次hash就能夠找到位置,若是發生沒有命中的狀況,則會引起性能會急劇降低,當在讀寫操做頻繁的場景,這點將成爲性能詬病。


Netty FastThreadLocal

Netty從新設計了更快的FastThreadLocal,主要實現涉及FastThreadLocalThread、FastThreadLocal和InternalThreadLocalMap類,FastThreadLocalThread是Thread類的簡單擴展,主要是爲了擴展threadLocalMap屬性。

public class FastThreadLocalThread extends Thread {

    private InternalThreadLocalMap threadLocalMap;

FastThreadLocal提供的接口和傳統的ThreadLocal一致,主要是set和get方法,用法也一致,不一樣地方在於FastThreadLocal的值是存儲在InternalThreadLocalMap這個結構裏面的,傳統的ThreadLocal性能槽點主要是在讀寫的時候hash計算和當hash沒有命中的時候發生的遍歷,咱們來看看FastThreadLocal的核心實現。先看看FastThreadLocal的構造方法:

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

實際上在構造FastThreadLocal實例的時候就決定了這個實例的索引,而索引的生成相關代碼咱們再看看:

public static int nextVariableIndex() {
    int index = nextIndex.getAndIncrement();
static final AtomicInteger nextIndex = new AtomicInteger();

nextIndex是InternalThreadLocalMap父類的一個全局靜態的AtomicInteger類型的對象,這意味着全部的FastThreadLocal實例將共同依賴這個指針來生成惟一的索引,並且是線程安全的。上面講過了InternalThreadLocalMap實例和Thread對象一一對應,而InternalThreadLocalMap維護着一個數組:

Object[] indexedVariables;

這個數組用來存儲跟同一個線程關聯的多個FastThreadLocal的值,因爲FastThreadLocal對應indexedVariables的索引是肯定的,所以在讀寫的時候將會發生隨機存取,很是快。

另外這裏有一個問題,nextIndex是靜態惟一的,而indexedVariables數組是實例對象的,所以我認爲隨着FastThreadLocal數量的遞增,這會形成空間的浪費。

性能數據:

我麼分析,性能問題主要存在的場景爲一個線程對應多個ThreadLocal實例,由於只有在這種場景下才會出現多個ThreadLocal對應的值存儲在同一個數組中,從而會有hash沒有命中或hash衝突的可能,我寫了兩段代碼來簡單測試傳統ThreadLocal和FastThreadLocal的性能,而後適當調整讀取數和ThreadLocal數進行對比:

代碼片斷1,傳統ThreadLocal測試:

public static void main(String ...s) {
    final int threadLocalCount = 1000;
    final ThreadLocal<String>[] caches = new ThreadLocal[threadLocalCount];
    final Thread mainThread = Thread.currentThread();
    for (int i=0;i<threadLocalCount;i++) {
        caches[i] = new ThreadLocal();
    }
    Thread t = new Thread(new Runnable() {
        @Override
        public void run() {
            for (int i=0;i<threadLocalCount;i++) {
                caches[i].set("float.lu");
            }
            long start = System.nanoTime();
            for (int i=0;i<threadLocalCount;i++) {
                for (int j=0;j<1000000;j++) {
                    caches[i].get();
                }
            }
            long end = System.nanoTime();
            System.out.println("take[" + TimeUnit.NANOSECONDS.toMillis(end - start) +
                    "]ms");
            LockSupport.unpark(mainThread);
        }

    });
    t.start();
    LockSupport.park(mainThread);
}

代碼片斷2,FastThreadLocal測試:

public static void main(String ...s) {
    final int threadLocalCount = 1000;
    final FastThreadLocal<String>[] caches = new FastThreadLocal[threadLocalCount];
    final Thread mainThread = Thread.currentThread();
    for (int i=0;i<threadLocalCount;i++) {
        caches[i] = new FastThreadLocal();
    }
    Thread t = new FastThreadLocalThread(new Runnable() {
        @Override
        public void run() {
            for (int i=0;i<threadLocalCount;i++) {
                caches[i].set("float.lu");
            }
            long start = System.nanoTime();
            for (int i=0;i<threadLocalCount;i++) {
                for (int j=0;j<1000000;j++) {
                    caches[i].get();
                }
            }
            long end = System.nanoTime();
            System.out.println("take[" + TimeUnit.NANOSECONDS.toMillis(end - start) +
                    "]ms");
            LockSupport.unpark(mainThread);
        }

    });
    t.start();
    LockSupport.park(mainThread);
}

兩段代碼邏輯相同,分別先進行稍稍的讀預熱,再適當調整對應的參數,分別統計5次結果:

1000個ThreadLocal對應一個線程對象對應一個線程對象的100w次的計時讀操做:

ThreadLocal:3767ms | 3636ms | 3595ms | 3610ms | 3719ms

FastThreadLocal: 15ms | 14ms | 13ms | 14ms | 14ms

1000個ThreadLocal對應一個線程對象對應一個線程對象的10w次的計時讀操做:

ThreadLocal:384ms | 378ms | 366ms | 647ms | 372ms

FastThreadLocal:14ms | 13ms | 13ms | 17ms | 13ms 

1000個ThreadLocal對應一個線程對象對應一個線程對象的1w次的計時讀操做:

ThreadLocal:43ms | 42ms | 42ms | 56ms | 45ms 

FastThreadLocal:15ms | 13ms | 11ms | 15ms | 11ms

100個ThreadLocal對應一個線程對象對應一個線程對象的1w次的計時讀操做:

ThreadLocal:16ms | 21ms | 18ms | 16ms | 18ms 

FastThreadLocal:15ms | 15ms | 15ms | 17ms | 18ms

上面的實驗數據能夠看出,當ThreadLocal數量和讀寫ThreadLocal的頻率較高的時候,傳統的ThreadLocal的性能降低速度比較快,而Netty實現的FastThreadLocal性能比較穩定。上面實驗模擬的場景不夠具體,可是已經在必定程度上咱們能夠認爲,FastThreadLocal相比傳統的的ThreadLocal在高併發高負載環境下表現的比較優秀。


本文由做者原創,僅由學習Netty源碼和進行性能實驗得出總結,若有問題還請多多指教。

相關文章
相關標籤/搜索