閱讀這篇文章之前,建議先閱讀和這篇文章關聯的內容。
1. 詳細剖析分散式微服務架構下網路通訊的底層實現原理(圖解)
2. (年薪60W的技巧)工作了5年,你真的理解Netty以及為什麼要用嗎?(深度乾貨)
4. BAT面試必問細節:關於Netty中的ByteBuf詳解
5. 通過大量實戰案例分解Netty中是如何解決拆包黏包問題的?
6. 基於Netty實現自定義訊息通訊協議(協議設計及解析應用實戰)
8. 手把手教你基於Netty實現一個基礎的RPC框架(通俗易懂)
9. (年薪60W分水嶺)基於Netty手寫實現RPC框架進階篇(帶註冊中心和註解)
FastThreadLocal的實現與J.U.C包中的ThreadLocal
非常類似。
瞭解過ThreadLocal
原理的同學應該都清楚,它有幾個關鍵的物件.
- Thread
- ThreadLocalMap
- ThreadLocal
同樣,Netty專門為FastThreadLocal
量身打造了FastThreadLocalThread
和InternalThreadLocalMap
兩個重要的類。下面我們看下這兩個類是如何實現的。
PS,如果不懂ThreadLocal的朋友,可以看我這篇文章:ThreadLocal的使用及原理分析
FastThreadLocalThread是對Thread
類的一層包裝,每個執行緒對應一個InternalThreadLocalMap
例項。只有FastThreadLocal
和FastThreadLocalThread
組合使用時,才能發揮 FastThreadLocal的效能優勢。首先看下FastThreadLocalThread
的原始碼定義:
public class FastThreadLocalThread extends Thread {
private InternalThreadLocalMap threadLocalMap;
// 省略其他程式碼
}
可以看出 FastThreadLocalThread 主要擴充套件了 InternalThreadLocalMap 欄位,我們可以猜測到 FastThreadLocalThread 主要使用 InternalThreadLocalMap 儲存資料,而不再是使用 Thread 中的 ThreadLocalMap。所以想知道 FastThreadLocalThread 高效能的奧祕,必須要了解 InternalThreadLocalMap 的設計原理。
InternalThreadLocalMap
public final class InternalThreadLocalMap extends UnpaddedInternalThreadLocalMap {
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;
private InternalThreadLocalMap() {
indexedVariables = newIndexedVariableTable();
}
private static Object[] newIndexedVariableTable() {
Object[] array = new Object[INDEXED_VARIABLE_TABLE_INITIAL_SIZE];
Arrays.fill(array, UNSET);
return array;
}
public static int lastVariableIndex() {
return nextIndex.get() - 1;
}
public static int nextVariableIndex() {
int index = nextIndex.getAndIncrement();
if (index < 0) {
nextIndex.decrementAndGet();
throw new IllegalStateException("too many thread-local indexed variables");
}
return index;
}
// 省略
}
從 InternalThreadLocalMap 內部實現來看,與 ThreadLocalMap 一樣都是採用陣列的儲存方式。
瞭解ThreadLocal的同學都知道,它內部也是採用陣列的方式來實現hash表,對於hash衝突,採用了線性探索的方式來實現。
但是 InternalThreadLocalMap 並沒有使用線性探測法來解決 Hash 衝突,而是在 FastThreadLocal 初始化的時候分配一個陣列索引 index,index 的值採用原子類 AtomicInteger 保證順序遞增,通過呼叫 InternalThreadLocalMap.nextVariableIndex() 方法獲得。然後在讀寫資料的時候通過陣列下標 index 直接定位到 FastThreadLocal 的位置,時間複雜度為 O(1)。如果陣列下標遞增到非常大,那麼陣列也會比較大,所以 FastThreadLocal 是通過空間換時間的思想提升讀寫效能。
下面通過一幅圖描述 InternalThreadLocalMap、index 和 FastThreadLocal 之間的關係。
通過上面 FastThreadLocal 的內部結構圖,我們對比下與 ThreadLocal 有哪些區別呢?
FastThreadLocal 使用 Object 陣列替代了 Entry 陣列,Object[0] 儲存的是一個Set<FastThreadLocal<?>> 集合。
從陣列下標 1 開始都是直接儲存的 value 資料,不再採用 ThreadLocal 的鍵值對形式進行儲存。
假設現在我們有一批資料需要新增到陣列中,分別為 value1、value2、value3、value4,對應的 FastThreadLocal 在初始化的時候生成的陣列索引分別為 1、2、3、4。如下圖所示。
至此,我們已經對 FastThreadLocal 有了一個基本的認識,下面我們結合具體的原始碼分析 FastThreadLocal 的實現原理。
FastThreadLocal的set方法原始碼分析
在講解原始碼之前,我們回過頭看下上文中的 ThreadLocal 示例,如果把示例中 ThreadLocal 替換成 FastThread,應當如何使用呢?
public class FastThreadLocalTest {
private static final FastThreadLocal<String> THREAD_NAME_LOCAL = new FastThreadLocal<>();
private static final FastThreadLocal<TradeOrder> TRADE_THREAD_LOCAL = new FastThreadLocal<>();
public static void main(String[] args) {
for (int i = 0; i < 2; i++) {
int tradeId = i;
String threadName = "thread-" + i;
new FastThreadLocalThread(() -> {
THREAD_NAME_LOCAL.set(threadName);
TradeOrder tradeOrder = new TradeOrder(tradeId, tradeId % 2 == 0 ? "已支付" : "未支付");
TRADE_THREAD_LOCAL.set(tradeOrder);
System.out.println("threadName: " + THREAD_NAME_LOCAL.get());
System.out.println("tradeOrder info:" + TRADE_THREAD_LOCAL.get());
}, threadName).start();
}
}
}
可以看出,FastThreadLocal 的使用方法幾乎和 ThreadLocal 保持一致,只需要把程式碼中 Thread、ThreadLocal 替換為 FastThreadLocalThread 和 FastThreadLocal 即可,Netty 在易用性方面做得相當棒。下面我們重點對示例中用得到 FastThreadLocal.set()/get() 方法做深入分析。
首先看下 FastThreadLocal.set() 的原始碼:
public final void set(V value) {
if (value != InternalThreadLocalMap.UNSET) {
InternalThreadLocalMap threadLocalMap = InternalThreadLocalMap.get();
setKnownNotUnset(threadLocalMap, value);
} else {
remove();
}
}
FastThreadLocal.set() 方法實現並不難理解,先抓住程式碼主幹,一步步進行拆解分析。set() 的過程主要分為三步:
- 判斷 value 是否為預設值,如果等於預設值,那麼直接呼叫 remove() 方法。這裡我們還不知道預設值和 remove() 之間的聯絡是什麼,我們暫且把 remove() 放在最後分析。
- 如果 value 不等於預設值,接下來會獲取當前執行緒的 InternalThreadLocalMap。
- 然後將 InternalThreadLocalMap 中對應資料替換為新的 value。
InternalThreadLocalMap.get()
先來看InternalThreadLocalMap.get()方法:
public static InternalThreadLocalMap get() {
Thread thread = Thread.currentThread();
if (thread instanceof FastThreadLocalThread) {
return fastGet((FastThreadLocalThread) thread);
} else {
return slowGet();
}
}
如果thread例項型別是FastThreadLocalThread,則呼叫fastGet()。
InternalThreadLocalMap.get() 邏輯很簡單.
- 如果當前執行緒是 FastThreadLocalThread 型別,那麼直接通過 fastGet() 方法獲取 FastThreadLocalThread 的 threadLocalMap 屬性即可
- 如果此時 InternalThreadLocalMap 不存在,直接建立一個返回。
關於 InternalThreadLocalMap 的初始化在上文中已經介紹過,它會初始化一個長度為 32 的 Object 陣列,陣列中填充著 32 個預設物件 UNSET 的引用。
private static InternalThreadLocalMap fastGet(FastThreadLocalThread thread) {
InternalThreadLocalMap threadLocalMap = thread.threadLocalMap();
if (threadLocalMap == null) {
thread.setThreadLocalMap(threadLocalMap = new InternalThreadLocalMap());
}
return threadLocalMap;
}
否則,則呼叫slowGet(),從程式碼實現來看,slowGet() 是針對非 FastThreadLocalThread 型別的執行緒發起呼叫時的一種兜底方案。如果當前執行緒不是 FastThreadLocalThread,內部是沒有 InternalThreadLocalMap 屬性的,Netty 在 UnpaddedInternalThreadLocalMap 中儲存了一個 JDK 原生的 ThreadLocal,ThreadLocal 中存放著 InternalThreadLocalMap,此時獲取 InternalThreadLocalMap 就退化成 JDK 原生的 ThreadLocal 獲取。
private static InternalThreadLocalMap slowGet() {
InternalThreadLocalMap ret = slowThreadLocalMap.get();
if (ret == null) {
ret = new InternalThreadLocalMap();
slowThreadLocalMap.set(ret);
}
return ret;
}
setKnownNotUnset
獲取 InternalThreadLocalMap 的過程已經講完了,下面看下 setKnownNotUnset() 如何將資料新增到 InternalThreadLocalMap 的。
private void setKnownNotUnset(InternalThreadLocalMap threadLocalMap, V value) {
if (threadLocalMap.setIndexedVariable(index, value)) {
addToVariablesToRemove(threadLocalMap, this);
}
}
setKnownNotUnset() 主要做了兩件事:
- 找到陣列下標 index 位置,設定新的 value。
- 將 FastThreadLocal 物件儲存到待清理的 Set 中。
首先我們看下第一步 threadLocalMap.setIndexedVariable() 的原始碼實現:
public boolean setIndexedVariable(int index, Object value) {
Object[] lookup = indexedVariables;
if (index < lookup.length) {
Object oldValue = lookup[index];
lookup[index] = value;
return oldValue == UNSET;
} else {
expandIndexedVariableTableAndSet(index, value);
return true;
}
}
indexedVariables 就是 InternalThreadLocalMap 中用於存放資料的陣列,如果陣列容量大於 FastThreadLocal 的 index 索引,那麼直接找到陣列下標 index 位置將新 value 設定進去,事件複雜度為 O(1)。在設定新的 value 之前,會將之前 index 位置的元素取出,如果舊的元素還是 UNSET 預設物件,那麼返回成功。
如果陣列容量不夠了怎麼辦呢?InternalThreadLocalMap 會自動擴容,然後再設定 value。接下來看看 expandIndexedVariableTableAndSet() 的擴容邏輯:
private void expandIndexedVariableTableAndSet(int index, Object value) {
Object[] oldArray = indexedVariables;
final int oldCapacity = oldArray.length;
int newCapacity = index;
newCapacity |= newCapacity >>> 1;
newCapacity |= newCapacity >>> 2;
newCapacity |= newCapacity >>> 4;
newCapacity |= newCapacity >>> 8;
newCapacity |= newCapacity >>> 16;
newCapacity ++;
Object[] newArray = Arrays.copyOf(oldArray, newCapacity);
Arrays.fill(newArray, oldCapacity, newArray.length, UNSET);
newArray[index] = value;
indexedVariables = newArray;
}
可以看出 InternalThreadLocalMap 實現陣列擴容幾乎和 HashMap 完全是一模一樣的,所以多讀原始碼還是可以給我們很多啟發的。InternalThreadLocalMap 以 index 為基準進行擴容,將陣列擴容後的容量向上取整為 2 的次冪。然後將原陣列內容拷貝到新的陣列中,空餘部分填充預設物件 UNSET,最終把新陣列賦值給 indexedVariables。
思考關於基準擴容
思考:為什麼 InternalThreadLocalMap 以 index 為基準進行擴容,而不是原陣列長度呢?
假設現在初始化了 70 個 FastThreadLocal,但是這些 FastThreadLocal 從來沒有呼叫過 set() 方法,此時陣列還是預設長度 32。當第 index = 70 的 FastThreadLocal 呼叫 set() 方法時,如果按原陣列容量 32 進行擴容 2 倍後,還是無法填充 index = 70 的資料。所以使用 index 為基準進行擴容可以解決這個問題,但是如果 FastThreadLocal 特別多,陣列的長度也是非常大的。
回到 setKnownNotUnset() 的主流程,向 InternalThreadLocalMap 新增完資料之後,接下就是將 FastThreadLocal 物件儲存到待清理的 Set 中。我們繼續看下 addToVariablesToRemove() 是如何實現的:
addToVariablesToRemove
private static void addToVariablesToRemove(InternalThreadLocalMap threadLocalMap, FastThreadLocal<?> variable) {
Object v = threadLocalMap.indexedVariable(variablesToRemoveIndex);
Set<FastThreadLocal<?>> variablesToRemove;
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);
}
variablesToRemoveIndex 是採用 static final 修飾的變數,在 FastThreadLocal 初始化時 variablesToRemoveIndex 被賦值為 0。InternalThreadLocalMap 首先會找到陣列下標為 0 的元素.
- 如果該元素是預設物件 UNSET 或者不存在,那麼會建立一個 FastThreadLocal 型別的 Set 集合,然後把 Set 集合填充到陣列下標 0 的位置。
- 如果陣列第一個元素不是預設物件 UNSET,說明 Set 集合已經被填充,直接強轉獲得 Set 集合即可。這就解釋了 InternalThreadLocalMap 的 value 資料為什麼是從下標為 1 的位置開始儲存了,因為 0 的位置已經被 Set 集合佔用了。
思考關於Set集合設計
思考:為什麼 InternalThreadLocalMap 要在陣列下標為 0 的位置存放一個 FastThreadLocal 型別的 Set 集合呢?這時候我們回過頭看下 remove() 方法。
public final void remove(InternalThreadLocalMap threadLocalMap) {
if (threadLocalMap == null) {
return;
}
Object v = threadLocalMap.removeIndexedVariable(index);
removeFromVariablesToRemove(threadLocalMap, this);
if (v != InternalThreadLocalMap.UNSET) {
try {
onRemoval((V) v);
} catch (Exception e) {
PlatformDependent.throwException(e);
}
}
}
在執行 remove 操作之前,會呼叫 InternalThreadLocalMap.getIfSet() 獲取當前 InternalThreadLocalMap。
有了之前的基礎,理解 getIfSet() 方法就非常簡單了。
- 如果是 FastThreadLocalThread 型別,直接取 FastThreadLocalThread 中 threadLocalMap 屬性。
- 如果是普通執行緒 Thread,從 ThreadLocal 型別的 slowThreadLocalMap 中獲取。
找到 InternalThreadLocalMap 之後,InternalThreadLocalMap 會從陣列中定位到下標 index 位置的元素,並將 index 位置的元素覆蓋為預設物件 UNSET。
接下來就需要清理當前的 FastThreadLocal 物件,此時 Set 集合就派上了用場,InternalThreadLocalMap 會取出陣列下標 0 位置的 Set 集合,然後刪除當前 FastThreadLocal。最後 onRemoval() 方法起到什麼作用呢?Netty 只是留了一處擴充套件,並沒有實現,使用者需要在刪除的時候做一些後置操作,可以繼承 FastThreadLocal 實現該方法。
FastThreadLocal.get()原始碼分析
再來看一下 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);
}
首先根據當前執行緒是否是 FastThreadLocalThread 型別找到 InternalThreadLocalMap,然後取出從陣列下標 index 的元素,如果 index 位置的元素不是預設物件 UNSET,說明該位置已經填充過資料,直接取出返回即可。
public Object indexedVariable(int index) {
Object[] lookup = indexedVariables;
return index < lookup.length? lookup[index] : UNSET;
}
如果 index 位置的元素是預設物件 UNSET,那麼需要執行初始化操作。可以看到,initialize() 方法會呼叫使用者重寫的 initialValue 方法構造需要儲存的物件資料.
private V initialize(InternalThreadLocalMap threadLocalMap) {
V v = null;
try {
v = initialValue();
} catch (Exception e) {
PlatformDependent.throwException(e);
}
threadLocalMap.setIndexedVariable(index, v);
addToVariablesToRemove(threadLocalMap, this);
return v;
}
initialValue方法的構造方式如下。
private final FastThreadLocal<String> threadLocal = new FastThreadLocal<String>() {
@Override
protected String initialValue() {
return "hello world";
}
};
構造完使用者物件資料之後,接下來就會將它填充到陣列 index 的位置,然後再把當前 FastThreadLocal 物件儲存到待清理的 Set 中。整個過程我們在分析 FastThreadLocal.set() 時都已經介紹過,就不再贅述了。
到此為止,FastThreadLocal 最核心的兩個方法 set()/get() 我們已經分析完了。下面有兩個問題我們再深入思考下。
- FastThreadLocal 真的一定比 ThreadLocal 快嗎?答案是不一定的,只有使用FastThreadLocalThread 型別的執行緒才會更快,如果是普通執行緒反而會更慢。
- FastThreadLocal 會浪費很大的空間嗎?雖然 FastThreadLocal 採用的空間換時間的思路,但是在 FastThreadLocal 設計之初就認為不會存在特別多的 FastThreadLocal 物件,而且在資料中沒有使用的元素只是存放了同一個預設物件的引用,並不會佔用太多記憶體空間。
版權宣告:本部落格所有文章除特別宣告外,均採用 CC BY-NC-SA 4.0 許可協議。轉載請註明來自
Mic帶你學架構
!
如果本篇文章對您有幫助,還請幫忙點個關注和贊,您的堅持是我不斷創作的動力。歡迎關注「跟著Mic學架構」公眾號公眾號獲取更多技術乾貨!