Netty原始碼閱讀入門實戰(十)-效能優化
1 效能優化工具類
FastThreadLocal
傳統的ThreadLocal
ThreadLocal最常用的兩個介面是set和get
最常見的應用場景為線上程上下文之間傳遞資訊,使得使用者不受複雜程式碼邏輯的影響
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例項,主要是維護著一個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:
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)
進入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數量的遞增,這會造成空間的浪費。