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內。到這裏咱們能夠總結一下幾點:服務器
一個ThreadLocal對於一個Thread對象來講只能存儲一個值,爲Object類型。網絡
多個ThreadLocal對於一個Thread對象,這些ThreadLocal和線程相關的值存儲在Thread對象關聯的ThreadLocalMap中。併發
使用擴展WeakReference的Entry做爲數據節點在必定程度上防止了內存泄露。框架
多個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循環:
因爲threadLocalHashCode&(len-1)的值對應的槽有內容,所以知足tab[i]!=null條件,進入for循環,若是知足條件且當前key不是當前threadlocal只能說明hash衝突了。
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來計算出位置,當沒有命中的時候,這裏沒有命中分爲三種狀況:
當前ThreadLocal以前沒有設值過,而且當前槽位沒有值。
當前槽位有值,可是對於的不是當前threadlocal,且那個ThreadLocal沒有被回收。
當前槽位有值,可是對於的不是當前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源碼和進行性能實驗得出總結,若有問題還請多多指教。