Netty 作爲高性能框架,對 JDK 中的很多類都進行了封裝了和優化,Netty 使用了 FastThreadLocalRunnable 對所有 DefaultThreadFactory 創建出來的 Runnable 都進行了包裝。 netty的FastThreadLocal和FastThreadLocalThread的實現相較於Thread和ThreadLocal不再發生內存泄漏,據說讀性能是 JDK 的 5 倍左右,寫入的速度也要快 20% 左右。
ThreadLocal
有人叫它線程本地變量,也叫做線程本地存儲。和線程同步機制大有不同,同步採用synchronized關鍵字和J.U.C中的Lock對象來實現,而加鎖的目的是爲了能讓多個線程安全的共享一個變量,ThreadLocal爲每個線程創建了自己獨有的變量副本,採用空間換時間思想。
{@code ThreadLocal} instances are typically private static fields in classes that wish to associate state with a thread (e.g., a user ID or Transaction ID)
用法
java8之前
private static final ThreadLocal<Integer> integerThreadLocal = new ThreadLocal<Integer>() {
@Override
protected Integer initialValue() {
return 100;
}
};
java8中
private static final ThreadLocal<Integer> integerThreadLocal = ThreadLocal.withInitial(() -> 100);
- get():返回此線程局部變量的當前線程副本中的值
- initialValue():返回此線程局部變量的當前線程的“初始值”,默認返回null,供子類重寫
- remove():移除此線程局部變量當前線程的值
- set(T value):將此線程局部變量的當前線程副本中的值設置爲指定值
實現原理
一個Thread
類中有這樣一個成員變量ThreadLocal.ThreadLocalMap,而
ThreadLocalMap是ThreadLocal實現線程隔離的精髓。
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);//key存儲的是ThreadLocal本身,而value則是實際存儲的值
else
createMap(t, value);
}
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
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);//如果hash 衝突了
}
//循環所有的元素,直到找到 key 對應的 entry,如果發現了某個元素的 key 是 null,順手調用 expungeStaleEntry 方法清理 所有 key 爲 null 的 entry
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;
}
//ThreadLocalMap的靜態內部類
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
而ThreadLocalMap.Entry實現了實現<k,v>
存儲,並繼承WeakReference
類,gc時判斷ThreadLocal是否已不可達。
ThreadLocalMap的key(Entry.referent爲ThreadLocal)設計WeakReference
這樣有什麼好處?
試想下,key
使用強引用:在當前ThreadLocal沒有被外部強引用時,ThreadLocalMap的Entry還保持着ThreadLocal的強引用,ThreadLocal不會被GC。如果沒有手動刪除,並且當前線程結束了,就導致了Entry的內存泄漏。(有點類似用static
修飾ThreadLocal的情況)
即便是弱引用也絕非完美:當ThreadLocal沒有被外部強引用的時候(比如線程結束)就會被GC回收,會發生:ThreadLocalMap會出現一個key
爲null
的Entry,但這個Entry的value
將永遠沒辦法被訪問到。如果當這個線程一直沒有結束,那這個key
爲null的
Entry因爲也存在強引用(Entry.value),而Entry被當前線程的ThreadLocalMap強引用(Entry[] table),導致這個Entry.value永遠無法被GC,造成內存泄漏。
強引用:普通的引用,強引用指向的對象不會被回收
軟引用:僅有軟引用指向的對象,只有發生gc且內存不足,纔會被回收
弱引用:僅有弱引用指向的對象(設置爲null時),只要發生gc就會被回收
key爲null的value問題好多,怎麼破?
雖然在ThreadLocalMap的設計中,已經考慮到這種情況的發生,它提供cleanSomeSlots()和expungeStaleEntry()方法都能清除key爲null的value,ThreadLocal的觸發點也很特別:set()、get()、remove()方法中都會調用它們。
也有不完美的地方(被動清除的方式並不是在所有情況下有效):
- 如果
ThreadLocal
的set()
,get()
,remove()
方法沒有被調用,就會導致value
的內存泄漏 - 用static修飾的ThreadLocal,導致ThreadLocal的生命週期和持有它的類一樣長,意味着這個ThreadLocal不會被GC。這種情況下,如果不手動刪除,Entry的key永遠不爲null,弱引用就失去了意義。
線程池
使用線程池時歸還線程之前記得清除ThreadLocalMap,要不然再取出該線程的時候,ThreadLocal變量還會存在。這就不僅僅是內存泄露的問題了,整個業務邏輯都可能會出錯。
解決方法參考:override ThreadPoolExecutor#afterExecute(r, t)
方法,對ThreadLocalMap進行清理。當然ThreadLocal最好還是不要和線程池一起使用。
FastThreadLocal
瞭解完jdk本身的ThreadLocal源碼,它使用太麻煩了,易出錯,性能也不高!netty對此進行了優化重構,並對jdk原生的線程進行了兼容!
FastThreadLocal有很多優點:
- 使用了單純的數組操作來替代了ThreadLocal的hash表操作,所以在高併發的情況下速度更快
- set操作,它直接根據index進行數組set。而ThreadLocal需要先根據ThreadLocal的hashcode計算數組下標,如果發生hash衝突且有無效的Entry時,還要進行Entry的清理和整理操作,不管是否衝突,都要進行一次log級別的Entry回收操作,所以肯定快不了
- get操作,它直接根據index進行獲取。而ThreadLocal需要先根據ThreadLocal的hashcode計算數組下標,然後再根據線性探測法進行get操作,如果不能根據直接索引獲取到value的話並且在向後循環遍歷的過程中發現了無效的Entry,則會進行無效Entry的清理和整理操作
- remove操作,它直接根據index從數組中刪除當前FastThreadLocal的value,然後從Set集合中刪除當前的FastThreadLocal,之後還可以進行刪除回調操作(功能增強)。而ThreadLocal需要先根據ThreadLocal的hashcode計算數組下標,然後再根據線性探測法進行remove操作,最後還需要進行無效Entry的整理和清理操作。
缺點也有:
FastThreadLocal較於ThreadLocal不好的地方就是內存佔用大,不會重複利用已經被刪除(用UNSET佔位)的數組位置,只會一味增大,是典型的“空間換時間”的操作。
使用
private static final FastThreadLocal<Integer> fastThreadLocal1 = new FastThreadLocal<Integer>(){
@Override
protected Integer initialValue() throws Exception {
return 100;
}
@Override
protected void onRemoved(Integer value) throws Exception {
System.out.println(value + ":我被刪除了");
}
};
@Test
public void testSetAndGetByCommonThread() {
Integer x = fastThreadLocal1.get();
fastThreadLocal1.remove();
x = fastThreadLocal1.get();//輸入null,而ThreadLocal不同一定是有
}
@Test
public void testSetAndGetByFastThreadLocalThread() {
new FastThreadLocalThread(()->{
Integer x = fastThreadLocal1.get();
fastThreadLocal1.set(200);
}).start();
}
private static final Executor executor = FastThreadExecutors.newCachedFastThreadPool("test");
@Test
public void testSetAndGetByFastThreadLocalThreadExecutor() {
executor.execute(()->{
Integer x = fastThreadLocal1.get();
String s = fastThreadLocal2.get();
fastThreadLocal1.set(200);
});
}
數據結構
對於jdk的ThreadLocal來講,其底層數據結構就是一個Entry[]數組,key爲ThreadLocal,value爲對應的值(hash表);通過線性探測法解決hash衝突。
先了解FastThreadLocalThread,每個FastThreadLocalThread內部都有一個InternalThreadLocalMap,而InternalThreadLocalMap 內部存儲的key就是FastThreadLocal value就是100(上面的),沒錯,和ThreadLocal的設計套路大同小異!但是InternalThreadLocalMap 底層是單純的簡單數組Object[],初始length==32,數組的第一個元素index=0存儲一個Set<FastThreadLocal<?>>
的set集合,存儲所有有效的FastThreadLocal。
每當有一個FastThreadLocal的value設置到數組中的時候,首先將當前的FastThreadLocal對象添加到Object[0]的set集合中,然後將FastThreadLocal的value存入Object[]的其餘位置(除0以外),而位置也很講究與FastThreadLocal實例屬性index對應。
//FastThreadLocal
public V get() {
// 1、獲取InternalThreadLocalMap
InternalThreadLocalMap threadLocalMap = InternalThreadLocalMap.get();
// 2、從InternalThreadLocalMap獲取索引爲index的value,如果該索引處的value是有效值,不是佔位值,則直接返回
Object value = threadLocalMap.indexedVariable(index);
if (value != InternalThreadLocalMap.UNSET) {
return (V) value;
}
// 3、indexedVariables[index]沒有設置有效值,執行初始化操作,獲取初始值
V initialValue = initialize(threadLocalMap);
// 4、註冊資源清理器:當該ftl所在的線程不強可達(沒有強引用指向該線程對象)時,清理其上當前ftl對象的value和set<FastThreadLocal<?>>中當前的ftl對象
registerCleaner(threadLocalMap);
return initialValue;
}
//兼容性
public static InternalThreadLocalMap get() {
Thread current = Thread.currentThread();
if (current instanceof FastThreadLocalThread) {
return fastGet((FastThreadLocalThread) current);
}
return slowGet();
}
private static InternalThreadLocalMap fastGet(FastThreadLocalThread current) {
InternalThreadLocalMap threadLocalMap = current.threadLocalMap();
if (threadLocalMap == null) {
threadLocalMap = new InternalThreadLocalMap();
current.setThreadLocalMap(threadLocalMap);
}
return threadLocalMap;
}
/**
* 兼容非FastThreadLocalThread
*/
private static final ThreadLocal<InternalThreadLocalMap> slowThreadLocalMap = new ThreadLocal<>();
private static InternalThreadLocalMap slowGet() {
InternalThreadLocalMap threadLocalMap = slowThreadLocalMap.get();
if (threadLocalMap == null) {
threadLocalMap = new InternalThreadLocalMap();
slowThreadLocalMap.set(threadLocalMap);
}
return threadLocalMap;
}
private void registerCleaner(InternalThreadLocalMap threadLocalMap) {
Thread current = Thread.currentThread();
// 如果已經開啓了自動清理功能 或者 已經對threadLocalMap中當前的FastThreadLocal開啓了清理線程
if (FastThreadLocalThread.willCleanupFastThreadLocals(current) || threadLocalMap.isCleanerFlags(index)) {
return;
}
// 設置是否已經開啓了對當前的FastThreadLocal清理線程的標誌
threadLocalMap.setCleanerFlags(index);
// 將當前線程和清理任務註冊到ObjectCleaner上去
ObjectCleaner.register(current, () -> remove(threadLocalMap));
}
回收機制
提供了三種回收機制:
- 自動,執行一個被FastThreadLocalRunnable wrap的Runnable任務,在任務執行完畢後會自動進行FastThreadLocal的清理
- 手動,FastThreadLocal和InternalThreadLocalMap都提供了remove方法,在合適的時候用戶可以(有的時候也是必須,例如普通線程的線程池使用FastThreadLocal)手動進行調用,進行顯示刪除
- 自動,爲當前線程的每一個FastThreadLocal註冊一個Cleaner,當線程對象不強可達的時候,該Cleaner線程會將當前線程的當前ftl進行回收
netty推薦使用前兩種方式,第三種方式需要另起線程,耗費資源,而且多線程就會造成一些資源競爭。