FastThreadLocal要了解下

Netty 作爲高性能框架,對 JDK 中的很多類都進行了封裝了和優化,Netty 使用了 FastThreadLocalRunnable 對所有 DefaultThreadFactory 創建出來的 Runnable 都進行了包裝。 netty的FastThreadLocalFastThreadLocalThread的實現相較於ThreadThreadLocal不再發生內存泄漏,據說讀性能是 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,而ThreadLocalMapThreadLocal實現線程隔離的精髓。

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沒有被外部強引用時,ThreadLocalMapEntry還保持着ThreadLocal的強引用,ThreadLocal不會被GC。如果沒有手動刪除,並且當前線程結束了,就導致了Entry的內存泄漏。(有點類似用static修飾ThreadLocal的情況)

即便是弱引用也絕非完美:當ThreadLocal沒有被外部強引用的時候(比如線程結束)就會被GC回收,會發生:ThreadLocalMap會出現一個keynullEntry,但這個Entryvalue將永遠沒辦法被訪問到。如果當這個線程一直沒有結束,那這個keynull的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()方法中都會調用它們。

也有不完美的地方(被動清除的方式並不是在所有情況下有效):

  • 如果ThreadLocalset()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));
}

回收機制

提供了三種回收機制:

  1. 自動,執行一個被FastThreadLocalRunnable wrap的Runnable任務,在任務執行完畢後會自動進行FastThreadLocal的清理
  2. 手動,FastThreadLocalInternalThreadLocalMap都提供了remove方法,在合適的時候用戶可以(有的時候也是必須,例如普通線程的線程池使用FastThreadLocal)手動進行調用,進行顯示刪除
  3. 自動,爲當前線程的每一個FastThreadLocal註冊一個Cleaner,當線程對象不強可達的時候,該Cleaner線程會將當前線程的當前ftl進行回收

netty推薦使用前兩種方式,第三種方式需要另起線程,耗費資源,而且多線程就會造成一些資源競爭。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章