主頁 >  其他 > 面試題 - ThreadLocal詳解

面試題 - ThreadLocal詳解

2020-10-01 13:59:39 其他

目錄(jdk1.8)

  • 一、什么是ThreadLocal
  • 二、ThreadLocal怎么用
  • 三、ThreadLocal的原理
  • 四、ThreadLocal原始碼分析
    • 1.ThreadLocal的內部屬性
    • 2.ThreadLocal 之 set() 方法
    • 3.ThreadLocal 之 get() 方法
    • 4.TreadLocal的remove方法
    • 5.內部類ThreadLocalMap的基本結構和原始碼分析
      • 5.1先看成員和結構部分
      • 5.2接著看ThreadLocalMap的建構式
      • 5.3ThreadLocalMap 之 set() 方法
      • 5.4ThreadLocalMap 之 getEntry() 方法
      • 5.5ThreadLocalMap 之 rehash() 方法
      • 5.6ThreadLocalMap 之 remove(key) 方法
  • 五、什么情況下ThreadLocal的使用會導致記憶體泄漏
  • 六、ThreadLocal的最佳實踐
  • 七、總結

一、什么是ThreadLocal

ThreadLocal 是 JDK java.lang 包下的一個類,是天然的執行緒安全的類,

1.ThreadLoca 是執行緒區域變數,這個變數與普通變數的區別,在于每個訪問該變數的執行緒,在執行緒內部都會
初始化一個獨立的變數副本,只有該執行緒可以訪問【get() or set()】該變數,ThreadLocal實體通常宣告
為 private static2.執行緒在存活并且ThreadLocal實體可被訪問時,每個執行緒隱含持有一個執行緒區域變數副本,當執行緒生命周期
結束時,ThreadLocal的實體的副本跟著執行緒一起消失,被GC垃圾回收(除非存在對這些副本的其他參考)

JDK 原始碼中決議:

/**
 * This class provides thread-local variables.  These variables differ from
 * their normal counterparts in that each thread that accesses one (via its
 * {@code get} or {@code set} method) has its own, independently initialized
 * copy of the variable.  {@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).
 * /

稍微翻譯一下:ThreadLocal提供執行緒區域變數,這些變數與正常的變數不同,因為每一個執行緒在訪問ThreadLocal實體的時候(通過其get或set方法)都有自己的、獨立初始化的變數副本,ThreadLocal實體通常是類中的私有靜態欄位,使用它的目的是希望將狀態(例如,用戶ID或事務ID)與執行緒關聯起來,

二、ThreadLocal怎么用

討論ThreadLocal用在什么地方前,我們先明確下,如果僅僅就一個執行緒,那么都不用談ThreadLocal的,ThreadLocal是用在多執行緒的場景的!!!

ThreadLocal歸納下來就3類用途:

  1. 保存執行緒背景關系資訊,在任意需要的地方可以獲取!!!
  2. 執行緒安全的,避免某些情況需要考慮執行緒安全必須同步帶來的性能損失!!!
  3. 執行緒間資料隔離

1.保存執行緒背景關系資訊,在任意需要的地方可以獲取!!!
由于ThreadLocal的特性,同一執行緒在某地方進行設定,在隨后的任意地方都可以獲取到,從而可以用來保存執行緒背景關系資訊,

常用的比如每個請求怎么把一串后續關聯起來,就可以用ThreadLocal進行set,在后續的任意需要記錄日志的方法里面進行get獲取到請求id,從而把整個請求串起來,

還有比如Spring的事務管理,用ThreadLocal存盤Connection,從而各個DAO可以獲取同一Connection,可以進行事務回滾,提交等操作,

2.執行緒安全的,避免某些情況需要考慮執行緒安全必須同步帶來的性能損失!!!
由于不需要共享資訊,自然就不存在競爭問題了,從而保證了某些情況下執行緒的安全,以及避免了某些情況需要考慮執行緒安全必須同步帶來的性能損失!!!

ThreadLocal局限性
ThreadLocal為解決多執行緒程式的并發問題提供了一種新的思路,但是ThreadLocal也有局限性,我們來看看阿里規范:
在這里插入圖片描述
這類場景阿里規范里面也提到了:
在這里插入圖片描述
ThreadLocal用法

public class MyThreadLocalDemo {

	private static ThreadLocal<String> threadLocal = new ThreadLocal<String>();

    public static void main(String[] args) throws InterruptedException {
        int threads = 9;
        MyThreadLocalDemo demo = new MyThreadLocalDemo();
        CountDownLatch countDownLatch = new CountDownLatch(threads);
        for (int i = 0; i < threads; i++) {
            Thread thread = new Thread(() -> {
                threadLocal.set(Thread.currentThread().getName());
                System.out.println("threadLocal.get()================>" + threadLocal.get());
                countDownLatch.countDown();
            }, "執行執行緒 - " + i);
            thread.start();
        }
        countDownLatch.await();
    }

}

代碼運行結果:

threadLocal.get()================>執行執行緒 - 1
threadLocal.get()================>執行執行緒 - 0
threadLocal.get()================>執行執行緒 - 3
threadLocal.get()================>執行執行緒 - 4
threadLocal.get()================>執行執行緒 - 5
threadLocal.get()================>執行執行緒 - 8
threadLocal.get()================>執行執行緒 - 7
threadLocal.get()================>執行執行緒 - 2
threadLocal.get()================>執行執行緒 - 6

Process finished with exit code 0

三、ThreadLocal的原理

在這里插入圖片描述

以兩個執行緒為例:

ThreadLocal雖然叫執行緒區域變數,但是實際上它并不存放任何的資訊,可以這樣理解:它是執行緒(Thread)操作ThreadLocalMap中存放的變數的橋梁,它主要提供了初始化、set()、get()、remove()幾個方法,這樣說可能有點抽象,下面畫個圖說明一下在執行緒中使用ThreadLocal實體的set()和get()方法的簡單流程圖,

假設我們有如下的代碼,主執行緒的執行緒名字是main(也有可能不是main):

public class Main {

    private static final ThreadLocal<String> LOCAL = new ThreadLocal<>();

    public static void main(String[] args) throws Exception{
        LOCAL.set("doge");
        System.out.println(LOCAL.get());
    }
}

在這里插入圖片描述
上面只描述了單執行緒的情況并且因為是主執行緒忽略了Thread t = new Thread()這一步,如果有多個執行緒會稍微復雜一些,但是原理是不變的,ThreadLocal實體總是通過Thread.currentThread()獲取到當前操作執行緒實體,然后去操作執行緒實體中的ThreadLocalMap型別的成員變數,因此它是一個橋梁,本身不具備存盤功能

四、ThreadLocal原始碼分析

從Thread原始碼入手:

public class Thread implements Runnable {
......
//與此執行緒有關的ThreadLocal值,該映射由ThreadLocal類維護,
ThreadLocal.ThreadLocalMap threadLocals = null;
//與此執行緒有關的InheritableThreadLocal值,該Map由InheritableThreadLocal類維護
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
......
}

從上面Thread類源代碼可以看出Thread類中有一個threadLocals和一個inheritableThreadLocals 變數,它們都是ThreadLocalMap型別的變數,默認情況下這兩個變數都是null,只有當前執行緒呼叫ThreadLocal類的Iset或get方法時才創建它們,實際上呼叫這兩個方法的時候,我們呼叫的是ThreadLocalMap類對應的get()、set()方法,
在這里插入圖片描述

在這里插入圖片描述

1.ThreadLocal的內部屬性

ThreadLocalMap 的 key 是 ThreadLocal,但它不會傳統的呼叫 ThreadLocal 的 hashCode 方法(繼承自Object 的 hashCode),而是呼叫 nextHashCode() ,具體運算如下:

public class ThreadLocal<T> {
	//獲取下一個ThreadLocal實體的哈希魔數
	private final int threadLocalHashCode = nextHashCode();
	
	//原子計數器,主要到它被定義為靜態
	private static AtomicInteger nextHashCode = new AtomicInteger();
	
	//哈希魔數(增長數),也是帶符號的32位整型值黃金分割值的取正
	private static final int HASH_INCREMENT = 0x61c88647;
	
	//生成下一個哈希魔數
	private static int nextHashCode() {
	    return nextHashCode.getAndAdd(HASH_INCREMENT);
	}
	...
}

這里需要注意一點,threadLocalHashCode是一個final的屬性,而原子計數器變數nextHashCode和生成下一個哈希魔數的方法nextHashCode()是靜態變數和靜態方法,靜態變數只會初始化一次,換而言之,每新建一個ThreadLocal實體,它內部的threadLocalHashCode就會增加0x61c88647,舉個例子:

//t1中的threadLocalHashCode變數為0x61c88647
ThreadLocal t1 = new ThreadLocal();
//t2中的threadLocalHashCode變數為0x61c88647 + 0x61c88647
ThreadLocal t2 = new ThreadLocal();
//t3中的threadLocalHashCode變數為0x61c88647 + 0x61c88647 + 0x61c88647
ThreadLocal t3 = new ThreadLocal();

threadLocalHashCode是下面的ThreadLocalMap結構中使用的哈希演算法的核心變數,對于每個ThreadLocal實體,它的threadLocalHashCode是唯一的,

這里寫個demo看一下基于魔數 1640531527 方式產生的hash分布多均勻:


public class ThreadLocalTest {
    public static void main(String[] args) {
        printAllSlot(8);
        printAllSlot(16);
        printAllSlot(32);
    }

    static void printAllSlot(int len) {
        System.out.println("********** len = " + len + " ************");
        for (int i = 1; i <= 64; i++) {
            ThreadLocal<String> t = new ThreadLocal<>();
            int slot = getSlot(t, len);
            System.out.print(slot + " ");
            if (i % len == 0) {
                System.out.println(); // 分組換行
            }
        }
    }

    /**
     * 獲取槽位
     *
     * @param t   ThreadLocal
     * @param len 模擬map的table的length
     * @throws Exception
     */
    static int getSlot(ThreadLocal<?> t, int len) {
        int hash = getHashCode(t);
        return hash & (len - 1);
    }

    /**
     * 反射獲取 threadLocalHashCode 欄位,因為其為private的
     */
    static int getHashCode(ThreadLocal<?> t) {
        Field field;
        try {
            field = t.getClass().getDeclaredField("threadLocalHashCode");
            field.setAccessible(true);
            return (int) field.get(t);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return 0;
    }
}

上述代碼模擬了 ThreadLocal 做為 key 的hashCode產生,看看完美槽位分配:

********** len = 8 ************
2 1 0 7 6 5 4 3 
2 1 0 7 6 5 4 3 
2 1 0 7 6 5 4 3 
2 1 0 7 6 5 4 3 
2 1 0 7 6 5 4 3 
2 1 0 7 6 5 4 3 
2 1 0 7 6 5 4 3 
2 1 0 7 6 5 4 3 
********** len = 16 ************
10 1 8 15 6 13 4 11 2 9 0 7 14 5 12 3 
10 1 8 15 6 13 4 11 2 9 0 7 14 5 12 3 
10 1 8 15 6 13 4 11 2 9 0 7 14 5 12 3 
10 1 8 15 6 13 4 11 2 9 0 7 14 5 12 3 
********** len = 32 ************
10 17 24 31 6 13 20 27 2 9 16 23 30 5 12 19 26 1 8 15 22 29 4 11 18 25 0 7 14 21 28 3 
10 17 24 31 6 13 20 27 2 9 16 23 30 5 12 19 26 1 8 15 22 29 4 11 18 25 0 7 14 21 28 3 

Process finished with exit code 0

2. ThreadLocal 之 set() 方法

ThreadLocal中set()方法的原始碼如下:


  protected T initialValue() {
        return null;
    }
    
   /**
    * 將此執行緒區域變數的當前執行緒副本設定為指定值,大多數子類將不需要
    * 重寫此方法,而僅依靠{@link #initialValue} 
    * 方法來設定執行緒區域變數的值,
    *
    * @param value 要存盤在此執行緒的thread-local副本中的值
    */
   public void set(T value) {
    //設定值前總是獲取當前執行緒實體
    Thread t = Thread.currentThread();
    //從當前執行緒實體中獲取threadLocals屬性
    ThreadLocalMap map = getMap(t);
    if (map != null)
         //threadLocals屬性不為null則覆寫key為當前的ThreadLocal實體,值為value
         map.set(this, value);
    else
    //threadLocals屬性為null,則創建ThreadLocalMap,第一個項的Key為當前的ThreadLocal實體,值為value
        createMap(t, value);
	}
	
	//這里看到獲取ThreadLocalMap實體時候總是從執行緒實體的成員變數獲取
 	ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
    
    //創建ThreadLocalMap實體的時候,會把新實體賦值到執行緒實體的threadLocals成員
 	void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

上面的程序原始碼很簡單,設定值的時候總是先獲取當前執行緒實體并且操作它的變數threadLocals,步驟是:

  1. 獲取當前運行執行緒的實體,
  2. 通過執行緒實體獲取執行緒實體成員threadLocals(ThreadLocalMap),如果為null,則創建一個新的ThreadLocalMap實體賦值到threadLocals,
  3. 通過threadLocals設定值value,如果原來的哈希槽已經存在值,則進行覆寫,

在這里插入圖片描述
在這里插入圖片描述
在這里插入圖片描述

3.ThreadLocal 之 get() 方法

ThreadLocal中get()方法的原始碼如下:


 	/**
     * 回傳此執行緒區域變數的當前執行緒副本中的值,如果該變數沒有當前執行緒的值,
     * 則首先通過呼叫{@link #initialValue}方法將其初始化為*回傳的值,
     *
     * @return 當前執行緒區域變數中的值
     */
     public T get() {
	    //獲取當前執行緒的實體
	    Thread t = Thread.currentThread();
	    ThreadLocalMap map = getMap(t);
	    if (map != null) {
	    //根據當前的ThreadLocal實體獲取ThreadLocalMap中的Entry,使用的是ThreadLocalMap的getEntry方法
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T) e.value;
             return result;
            }
        }
	    //執行緒實體中的threadLocals為null,則呼叫initialValue方法,并且創建ThreadLocalMap賦值到threadLocals
	    return setInitialValue();
	}
	
	private T setInitialValue() {
	    // 呼叫initialValue方法獲取值
	    T value = initialValue();
	    Thread t = Thread.currentThread();
	    ThreadLocalMap map = getMap(t);
	    // ThreadLocalMap如果未初始化則進行一次創建,已初始化則直接設定值
	    if (map != null)
	        map.set(this, value);
	    else
	        createMap(t, value);
	    return value;
	}
	
	protected T initialValue() {
       return null;
    }

initialValue()方法默認回傳null,如果ThreadLocal實體沒有使用過set()方法直接使用get()方法,那么ThreadLocalMap中的此ThreadLocal為Key的項會把值設定為initialValue()方法的回傳值,如果想改變這個邏輯可以對initialValue()方法進行覆寫,
在這里插入圖片描述

4.TreadLocal的remove方法

ThreadLocal中remove()方法的原始碼如下:

public void remove() {
    //獲取Thread實體中的ThreadLocalMap
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
       //根據當前ThreadLocal作為Key對ThreadLocalMap的元素進行移除
       m.remove(this);
}

在這里插入圖片描述

這里羅列了 ThreadLocal 的幾個public方法,其實所有作業最終都落到了 ThreadLocalMap 的頭上,ThreadLocal 僅僅是從當前執行緒取到 ThreadLocalMap 而已,具體執行,請看下面對 ThreadLocalMap 的分析,

5.內部類ThreadLocalMap的基本結構和原始碼分析

ThreadLocalMap 是ThreadLocal 內部的一個Map實作,然而它并沒有實作任何集合的介面規范,因為它僅供內部使用,資料結構采用 陣列 + 開方地址法,Entry 繼承 WeakReference,是基于 ThreadLocal 這種特殊場景實作的 Map,它的實作方式很值得研究,

ThreadLocal內部類ThreadLocalMap使用了默認修飾符,也就是包(包私有)可訪問的,ThreadLocalMap內部定義了一個靜態類Entry,我們重點看下ThreadLocalMap的原始碼,

5.1先看成員和結構部分

/**
 * ThreadLocalMap是一個定制的散列映射,僅適用于維護執行緒本地變數,
 * 它的所有方法都是定義在ThreadLocal類之內,
 * 它是包私有的,所以在Thread類中可以定義ThreadLocalMap作為變數,
 * 為了處理非常大(指的是值)和長時間的用途,哈希表的Key使用了弱參考(WeakReferences),
 * 參考的佇列(弱參考)不再被使用的時候,對應的過期的條目就能通過主動洗掉移出哈希表,
 */
static class ThreadLocalMap {

    //注意這里的Entry的Key為WeakReference<ThreadLocal<?>>
    static class Entry extends WeakReference<ThreadLocal<?>> {

        //這個是真正的存放的值
        Object value;
        // Entry的Key就是ThreadLocal實體本身,Value就是輸入的值
        Entry(ThreadLocal<?> k, Object v) {
                    super(k);
                    value = v;
        }
    }
    //初始化容量,必須是2的冪次方
    private static final int INITIAL_CAPACITY = 16;

    //哈希(Entry)表,必須時擴容,長度必須為2的冪次方
    private Entry[] table;

    //哈希表中元素(Entry)的個數
    private int size = 0;

    //下一次需要擴容的閾值,默認值為0
    private int threshold;

    //設定下一次需要擴容的閾值,設定值為輸入值len的三分之二
    private void setThreshold(int len) {
        threshold = len * 2 / 3;
    }

    // 以len為模增加i
    private static int nextIndex(int i, int len) {
        return ((i + 1 < len) ? i + 1 : 0);
    }

    // 以len為模減少i
    private static int prevIndex(int i, int len) {
        return ((i - 1 >= 0) ? i - 1 : len - 1);
    }
}
  1. 這里注意到十分重要的一點:ThreadLocalMap$Entry是WeakReference(弱參考),并且鍵值Key為ThreadLocal<?>實體本身,這里使用了無限定的泛型通配符,
  2. ThreadLocalMap 的 key 是 ThreadLocal,但它不會傳統的呼叫 ThreadLocal 的 hashCode 方法(繼承自Object 的 hashCode),而是呼叫 nextHashCode()

5.2接著看ThreadLocalMap的建構式

// 構造ThreadLocal時候使用,對應ThreadLocal的實體方法void createMap(Thread t, T firstValue)
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
    // 哈希表默認容量為16
    table = new Entry[INITIAL_CAPACITY];
    // 計算第一個元素的哈希碼
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
    table[i] = new Entry(firstKey, firstValue);
    size = 1;
    setThreshold(INITIAL_CAPACITY);
}

// 構造InheritableThreadLocal時候使用,基于父執行緒的ThreadLocalMap里面的內容進行
// 提取放入新的ThreadLocalMap的哈希表中
// 對應ThreadLocal的靜態方法static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap)
private ThreadLocalMap(ThreadLocalMap parentMap) {
    Entry[] parentTable = parentMap.table;
    int len = parentTable.length;
    setThreshold(len);
    table = new Entry[len];
    // 基于父ThreadLocalMap的哈希表進行拷貝
    for (Entry e : parentTable) {
        if (e != null) {
            @SuppressWarnings("unchecked")
            ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
            if (key != null) {
                Object value = key.childValue(e.value);
                Entry c = new Entry(key, value);
                int h = key.threadLocalHashCode & (len - 1);
                while (table[h] != null)
                    h = nextIndex(h, len);
                table[h] = c;
                size++;
            }
        }
    }
}

這里注意一下,ThreadLocal的set()方法呼叫的時候會懶初始化一個ThreadLocalMap并且放入第一個元素,而ThreadLocalMap的私有構造是提供給靜態方法ThreadLocal#createInheritedMap()使用的,

5.3ThreadLocalMap 之 set() 方法

private void set(ThreadLocal<?> key, Object value) {

    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1); // 用key的hashCode計算槽位
    // hash沖突時,使用開放地址法
    // 因為獨特和hash演算法,導致hash沖突很少,一般不會走進這個for回圈
    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        ThreadLocal<?> k = e.get();

        if (k == key) { // key 相同,則覆寫value
            e.value = value; 
            return;
        }

        if (k == null) { // key = null,說明 key 已經被回收了,進入替換方法
            replaceStaleEntry(key, value, i);
            return;
        }
    }
    // 新增 Entry
    tab[i] = new Entry(key, value);
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold) // 清除一些過期的值,并判斷是否需要擴容
        rehash(); // 擴容
}

這個 set 方法涵蓋了很多關鍵點:

  1. 開放地址法:與我們常用的Map不同,java里大部分Map都是用鏈表發解決hash沖突的,而 ThreadLocalMap 采用的是開發地址法,
  2. hash演算法:hash值演算法的精妙之處上面已經講了,均勻的 hash 演算法使其可以很好的配合開方地址法使用;
  3. 過期值清理

下面對 set 方法里面的幾個關鍵方法展開:

1.replaceStaleEntry()
因為開發地址發的使用,導致 replaceStaleEntry 這個方法有些復雜,它的清理作業會涉及到slot前后的非null的slot,

//這里個方法比較長,作用是替換哈希碼為staleSlot的哈希槽中Entry的值
private void replaceStaleEntry(ThreadLocal<?> key, Object value,
                               int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;
    Entry e;

    // 往前尋找過期的slot
    int slotToExpunge = staleSlot;
    for (int i = prevIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = prevIndex(i, len))
        if (e.get() == null)
            slotToExpunge = i;

    // 找到 key 或者 直到 遇到null 的slot 才終止回圈
    // 遍歷staleSlot之后的哈希槽,如果Key匹配則用輸入值替換
    for (int i = nextIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();

        // 如果找到了key,那么需要將它與過期的 slot 交換來維護哈希表的順序,
        // 然后可以將新過期的 slot 或其上面遇到的任何其他過期的 slot 
        // 給 expungeStaleEntry 以清除或 rehash 這個 run 中的所有其他entries,

        if (k == key) {
            e.value = value;

            tab[i] = tab[staleSlot];
            tab[staleSlot] = e;

            // 如果存在,則開始清除前面過期的entry
            if (slotToExpunge == staleSlot)
                slotToExpunge = i;
            cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
            return;
        }

        // 如果我們沒有在向前掃描中找到過期的條目,
        // 那么在掃描 key 時看到的第一個過期 entry 是仍然存在于 run 中的條目,
        if (k == null && slotToExpunge == staleSlot)
            slotToExpunge = i;
    }

    // 如果沒有找到 key,那么在 slot 中創建新entry
    tab[staleSlot].value = null;
    tab[staleSlot] = new Entry(key, value);

    // 如果還有其他過期的entries存在 run 中,則清除他們
    if (slotToExpunge != staleSlot)
        cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}

上文中的 run 不好翻譯,理解為開放地址中一個slot中前后不為null的連續entry

2.cleanSomeSlots()
cleanSomeSlots 清除一些slot(一些?是不是有點模糊,到底是哪些?)

//清理第i個哈希槽之后的n個哈希槽,如果遍歷的時候發現Entry的Key為null,則n會重置為哈希表的長度,
//expungeStaleEntry有可能會重哈希使得哈希表長度發生變化
private boolean cleanSomeSlots(int i, int n) {
    boolean removed = false;
    Entry[] tab = table;
    int len = tab.length;
    do {
        i = nextIndex(i, len);
        Entry e = tab[i];
        if (e != null && e.get() == null) {
            n = len;
            removed = true;
            i = expungeStaleEntry(i); // 清除方法 
        }
    } while ( (n >>>= 1) != 0);  // n = n / 2, 對數控制回圈 
    return removed;
}

當新元素被添加時,或者另一個過期元素已被洗掉時,會呼叫cleanSomeSlots,該方法會試探性地掃描一些 entry 尋找過期的條目,它執行 對數 數量的掃描,是一種 基于不掃描(快速但保留垃圾)和 所有元素掃描之間的平衡,

上面說到的對數數量是多少?回圈次數 = log2(N) (log以2為底N的對數),此處N是map的size,如:

log2(4= 2
log2(5= 2
log2(18= 4

因此,此方法并沒有真正的清除,只是找到了要清除的位置,而真正的清除在 expungeStaleEntry(int staleSlot) 里面

3.expungeStaleEntry(int staleSlot)

這里是真正的清除,并且不要被方法名迷惑,不僅僅會清除當前過期的slot,還回往后查找直到遇到null的slot為止,開放地址法的清除也較難理解,清除當前slot后還有往后進行rehash,

//對當前哈希表中所有的Key為null的Entry呼叫expungeStaleEntry
// 1.清空staleSlot對應哈希槽的Key和Value
// 2.對staleSlot到下一個空的哈希槽之間的所有可能沖突的哈希表部分槽進行重哈希,置空Key為null的槽
// 3.注意回傳值是staleSlot之后的下一個空的哈希槽的哈希碼
private int expungeStaleEntry(int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;

    // 清空staleSlot對應哈希槽的Key和Value
    tab[staleSlot].value = null;
    tab[staleSlot] = null;
    size--;

    // Rehash 直到 null 的 slot
    Entry e;
    int i;
    for (i = nextIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();
        if (k == null) {//空key直接清除
            e.value = null;
            tab[i] = null;
            size--;
        } else {//key非空,則Rehash
            int h = k.threadLocalHashCode & (len - 1);
            if (h != i) {
                tab[i] = null;

                while (tab[h] != null)
                    h = nextIndex(h, len);
                tab[h] = e;
            }
        }
    }
    return i;
}

5.4ThreadLocalMap 之 getEntry() 方法

getEntry() 主要是在 ThreadLocal 的 get() 方法里被呼叫

/**
 * 這個方法主要給`ThreadLocal#get()`呼叫,通過當前ThreadLocal實體獲取哈希表中對應的Entry
 *
 */
private Entry getEntry(ThreadLocal<?> key) {
    // 計算Entry的哈希值
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i]; 
    if (e != null && e.get() == key)//無hash沖突情況
        return e;
    else  // 注意這里,如果e為null或者Key對不上,表示:有hash沖突情況,會呼叫getEntryAfterMiss
        return getEntryAfterMiss(key, i, e);
}

// 如果Key在哈希表中找不到哈希槽的時候會呼叫此方法
// 這個方法是在遇到 hash 沖突時往后繼續查找,并且會清除查找路上遇到的過期slot,
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
    Entry[] tab = table;
    int len = tab.length;
    // 這里會通過nextIndex嘗試遍歷整個哈希表,如果找到匹配的Key則回傳Entry
    // 如果哈希表中存在Key == null的情況,呼叫expungeStaleEntry進行清理
    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;
}

5.5ThreadLocalMap 之 rehash() 方法

// 重哈希,必要時進行擴容
private void rehash() {
    // 清理所有空的哈希槽,并且進行重哈希
    expungeStaleEntries();

    // Use lower threshold for doubling to avoid hysteresis
    // 在上面的清除程序中,size會減小,在此處重新計算是否需要擴容
    // 并沒有直接使用threshold,而是用較低的threshold (約 threshold 的 3/4)提前觸發resize
    if (size >= threshold - threshold / 4)
        resize();
}

// 對當前哈希表中所有的Key為null的Entry呼叫expungeStaleEntry
private void expungeStaleEntries() {
    Entry[] tab = table;
    int len = tab.length;
    for (int j = 0; j < len; j++) {
        Entry e = tab[j];
        if (e != null && e.get() == null)
            expungeStaleEntry(j);
    }
}

// 擴容,簡單的擴大2倍的容量        
private void resize() {
    Entry[] oldTab = table;
    int oldLen = oldTab.length;
    int newLen = oldLen * 2;
    Entry[] newTab = new Entry[newLen];
    int count = 0;

    for (Entry e : oldTab) {
        if (e != null) {
            ThreadLocal<?> k = e.get();
            if (k == null) {
                e.value = null; // Help the GC
            } else {
                int h = k.threadLocalHashCode & (newLen - 1);
                while (newTab[h] != null)
                     h = nextIndex(h, newLen);
                newTab[h] = e;
                count++;
            }
        }
    }

    setThreshold(newLen);
    size = count;
    table = newTab;
}

PS :ThreadLocalMap 沒有 影響因子 的欄位,是采用直接設定 threshold 的方式,threshold = len * 2 / 3,相當于不可修改的影響因子為 2/3,比 HashMap 的默認 0.75 要低,這也是減少hash沖突的方式,

5.6ThreadLocalMap 之 remove(key) 方法

	/**
	 * Remove the entry for key.
	 */
	private void remove(ThreadLocal<?> key) {
	    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)]) {
	        if (e.get() == key) {
	            e.clear();
	            expungeStaleEntry(i);
	            return;
	        }
	    }
	}

remove 方法是洗掉特定的 ThreadLocal,建議在 ThreadLocal 使用完后一定要執行此方法,

五、什么情況下ThreadLocal的使用會導致記憶體泄漏

其實ThreadLocal本身不存放任何的資料,而ThreadLocal中的資料實際上是存放在執行緒實體中,從實際來看是執行緒記憶體泄漏,底層來看是Thread物件中的成員變數threadLocals持有大量的K-V結構,并且執行緒一直處于活躍狀態導致變數threadLocals無法釋放被回收,threadLocals持有大量的K-V結構這一點的前提是要存在大量的ThreadLocal實體的定義,一般來說,一個應用不可能定義大量的ThreadLocal,所以一般的泄漏源是執行緒一直處于活躍狀態導致變數threadLocals無法釋放被回收,但是我們知道,·ThreadLocalMap·中的Entry結構的Key用到了弱參考(·WeakReference<ThreadLocal<?>>·),當沒有強參考來參考ThreadLocal實體的時候,JVM的GC會回收ThreadLocalMap中的這些Key,此時,ThreadLocalMap中會出現一些Key為null,但是Value不為null的Entry項,這些Entry項如果不主動清理,就會一直駐留在ThreadLocalMap中,也就是為什么ThreadLocal中get()、set()、remove()這些方法中都存在清理ThreadLocalMap實體key為null的代碼塊,總結下來,記憶體泄漏可能出現的地方是:

大量地(靜態)初始化ThreadLocal實體,初始化之后不再呼叫get()、set()、remove()方法,

初始化了大量的ThreadLocal,這些ThreadLocal中存放了容量大的Value,并且使用了這些ThreadLocal實體的執行緒一直處于活躍的狀態,
ThreadLocal中一個設計亮點是ThreadLocalMap中的Entry結構的Key用到了弱參考,試想如果使用強參考,等于ThreadLocalMap中的所有資料都是與Thread的生命周期系結,這樣很容易出現因為大量執行緒持續活躍導致的記憶體泄漏,使用了弱參考的話,JVM觸發GC回收弱參考后,ThreadLocal在下一次呼叫get()、set()、remove()方法就可以洗掉那些ThreadLocalMap中Key為null的值,起到了惰性洗掉釋放記憶體的作用,

其實ThreadLocal在設定內部類ThreadLocal.ThreadLocalMap中構建的Entry哈希表已經考慮到記憶體泄漏的問題,所以ThreadLocal.ThreadLocalMap$Entry類設計為弱參考,類簽名為static class Entry extends WeakReference<ThreadLocal<?>>,之前一篇文章介紹過,如果弱參考關聯的物件如果置為null,那么該弱參考會在下一次GC時候回收弱參考關聯的物件,舉個例子:

public class ThreadLocalMain {

    private static ThreadLocal<Integer> TL_1 = new ThreadLocal<>();

    public static void main(String[] args) throws Exception {
        TL_1.set(1);
        TL_1 = null;
        System.gc();
        Thread.sleep(300);
    }
}

這種情況下,TL_1這個ThreadLocal在主動GC之后,執行緒系結的ThreadLocal.ThreadLocalMap實體中的Entry哈希表中原來的TL_1所在的哈希槽Entry的參考持有值referent(繼承自WeakReference)會變成null,但是Entry中的value是強參考,還存放著TL_1這個ThreadLocal未回收之前的值,這些被”孤立”的哈希槽Entry就是前面說到的要惰性洗掉的哈希槽,

六、ThreadLocal的最佳實踐

其實ThreadLocal的最佳實踐很簡單:

  • 每次使用完ThreadLocal實體,都呼叫它的remove()方法,清除Entry中的資料,

呼叫remove()方法最佳時機是執行緒運行結束之前的finally代碼塊中呼叫,這樣能完全避免操作不當導致的記憶體泄漏,這種主動清理的方式比惰性洗掉有效,

七、總結

ThreadLocal執行緒本地變數是執行緒實體傳遞和存盤共享變數的橋梁,真正的共享變數還是存放在執行緒實體本身的屬性中,ThreadLocal里面的基本邏輯并不復雜,但是一旦涉及到性能影響、記憶體回收(弱參考)和惰性洗掉等環節,其實它考慮到的東西還是相對全面而且有效的,

ThreadLocalMap 的 value 清理觸發時間:

  1. set(ThreadLocal<?> key, Object value)
    若無hash沖突,則先向后檢測log2(N)個位置,發現過期 slot 則清除,如果沒有任何 slot 被清除,則判斷 size >= threshold,超過閥值會進行 rehash(),rehash()會清除所有過期的value;
  2. getEntry(ThreadLocal<?> key) (ThreadLocal 的 get() 方法呼叫)
    如果沒有直接在hash計算的 slot 中找到entry, 則需要向后繼續查找(直到null為止),查找期間發現的過期 slot 會被清除;
  3. remove(ThreadLocal<?> key)
    remove 不僅會清除需要清除的 key,還是清除hash沖突的位置的已過期的 key;
    清晰了以上程序,相信對于 ThreadLocal 的 記憶體溢位問題會有自己的看法,在實際開發中,不應亂用 ThreadLocal ,如果使用 ThreadLocal 發生了記憶體溢位,那應該考慮是否使用合理,

PS:這里的清除并不代表被回收,只是把 value 置為 null,value 的具體回收時間由 垃圾收集器 決定,

參考鏈接:

https://www.jianshu.com/p/56f64e3c1b6c
https://blog.51cto.com/14409778/2416835?source=dra

轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/145358.html

標籤:其他

上一篇:工業網路國內外技術與標準化現狀

下一篇:高性能快取實踐-快取擊穿

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 網閘典型架構簡述

    網閘架構一般分為兩種:三主機的三系統架構網閘和雙主機的2+1架構網閘。 三主機架構分別為內端機、外端機和仲裁機。三機無論從軟體和硬體上均各自獨立。首先從硬體上來看,三機都用各自獨立的主板、記憶體及存盤設備。從軟體上來看,三機有各自獨立的作業系統。這樣能達到完全的三機獨立。對于“2+1”系統,“2”分為 ......

    uj5u.com 2020-09-10 02:00:44 more
  • 如何從xshell上傳檔案到centos linux虛擬機里

    如何從xshell上傳檔案到centos linux虛擬機里及:虛擬機CentOs下執行 yum -y install lrzsz命令,出現錯誤:鏡像無法找到軟體包 前言 一、安裝lrzsz步驟 二、上傳檔案 三、遇到的問題及解決方案 總結 前言 提示:其實很簡單,往虛擬機上安裝一個上傳檔案的工具 ......

    uj5u.com 2020-09-10 02:00:47 more
  • 一、SQLMAP入門

    一、SQLMAP入門 1、判斷是否存在注入 sqlmap.py -u 網址/id=1 id=1不可缺少。當注入點后面的引數大于兩個時。需要加雙引號, sqlmap.py -u "網址/id=1&uid=1" 2、判斷文本中的請求是否存在注入 從文本中加載http請求,SQLMAP可以從一個文本檔案中 ......

    uj5u.com 2020-09-10 02:00:50 more
  • Metasploit 簡單使用教程

    metasploit 簡單使用教程 浩先生, 2020-08-28 16:18:25 分類專欄: kail 網路安全 linux 文章標簽: linux資訊安全 編輯 著作權 metasploit 使用教程 前言 一、Metasploit是什么? 二、準備作業 三、具體步驟 前言 Msfconsole ......

    uj5u.com 2020-09-10 02:00:53 more
  • 游戲逆向之驅動層與用戶層通訊

    驅動層代碼: #pragma once #include <ntifs.h> #define add_code CTL_CODE(FILE_DEVICE_UNKNOWN,0x800,METHOD_BUFFERED,FILE_ANY_ACCESS) /* 更多游戲逆向視頻www.yxfzedu.com ......

    uj5u.com 2020-09-10 02:00:56 more
  • 北斗電力時鐘(北斗授時服務器)讓網路資料更精準

    北斗電力時鐘(北斗授時服務器)讓網路資料更精準 北斗電力時鐘(北斗授時服務器)讓網路資料更精準 京準電子科技官微——ahjzsz 近幾年,資訊技術的得了快速發展,互聯網在逐漸普及,其在人們生活和生產中都得到了廣泛應用,并且取得了不錯的應用效果。計算機網路資訊在電力系統中的應用,一方面使電力系統的運行 ......

    uj5u.com 2020-09-10 02:01:03 more
  • 【CTF】CTFHub 技能樹 彩蛋 writeup

    ?碎碎念 CTFHub:https://www.ctfhub.com/ 筆者入門CTF時時剛開始刷的是bugku的舊平臺,后來才有了CTFHub。 感覺不論是網頁UI設計,還是題目質量,賽事跟蹤,工具軟體都做得很不錯。 而且因為獨到的金幣制度的確讓人有一種想去刷題賺金幣的感覺。 個人還是非常喜歡這個 ......

    uj5u.com 2020-09-10 02:04:05 more
  • 02windows基礎操作

    我學到了一下幾點 Windows系統目錄結構與滲透的作用 常見Windows的服務詳解 Windows埠詳解 常用的Windows注冊表詳解 hacker DOS命令詳解(net user / type /md /rd/ dir /cd /net use copy、批處理 等) 利用dos命令制作 ......

    uj5u.com 2020-09-10 02:04:18 more
  • 03.Linux基礎操作

    我學到了以下幾點 01Linux系統介紹02系統安裝,密碼啊破解03Linux常用命令04LAMP 01LINUX windows: win03 8 12 16 19 配置不繁瑣 Linux:redhat,centos(紅帽社區版),Ubuntu server,suse unix:金融機構,證券,銀 ......

    uj5u.com 2020-09-10 02:04:30 more
  • 05HTML

    01HTML介紹 02頭部標簽講解03基礎標簽講解04表單標簽講解 HTML前段語言 js1.了解代碼2.根據代碼 懂得挖掘漏洞 (POST注入/XSS漏洞上傳)3.黑帽seo 白帽seo 客戶網站被黑帽植入劫持代碼如何處理4.熟悉html表單 <html><head><title>TDK標題,描述 ......

    uj5u.com 2020-09-10 02:04:36 more
最新发布
  • 2023年最新微信小程式抓包教程

    01 開門見山 隔一個月發一篇文章,不過分。 首先回顧一下《微信系結手機號資料庫被脫庫事件》,我也是第一時間得知了這個訊息,然后跟蹤了整件事情的經過。下面是這起事件的相關截圖以及近日流出的一萬條資料樣本: 個人認為這件事也沒什么,還不如關注一下之前45億快遞資料查詢渠道疑似在近日復活的訊息。 訊息是 ......

    uj5u.com 2023-04-20 08:48:24 more
  • web3 產品介紹:metamask 錢包 使用最多的瀏覽器插件錢包

    Metamask錢包是一種基于區塊鏈技術的數字貨幣錢包,它允許用戶在安全、便捷的環境下管理自己的加密資產。Metamask錢包是以太坊生態系統中最流行的錢包之一,它具有易于使用、安全性高和功能強大等優點。 本文將詳細介紹Metamask錢包的功能和使用方法。 一、 Metamask錢包的功能 數字資 ......

    uj5u.com 2023-04-20 08:47:46 more
  • vulnhub_Earth

    前言 靶機地址->>>vulnhub_Earth 攻擊機ip:192.168.20.121 靶機ip:192.168.20.122 參考文章 https://www.cnblogs.com/Jing-X/archive/2022/04/03/16097695.html https://www.cnb ......

    uj5u.com 2023-04-20 07:46:20 more
  • 從4k到42k,軟體測驗工程師的漲薪史,給我看哭了

    清明節一過,盲猜大家已經無心上班,在數著日子準備過五一,但一想到銀行卡里的余額……瞬間心情就不美麗了。最近,2023年高校畢業生就業調查顯示,本科畢業月平均起薪為5825元。調查一出,便有很多同學表示自己又被平均了。看著這一資料,不免讓人想到前不久中國青年報的一項調查:近六成大學生認為畢業10年內會 ......

    uj5u.com 2023-04-20 07:44:00 more
  • 最新版本 Stable Diffusion 開源 AI 繪畫工具之中文自動提詞篇

    🎈 標簽生成器 由于輸入正向提示詞 prompt 和反向提示詞 negative prompt 都是使用英文,所以對學習母語的我們非常不友好 使用網址:https://tinygeeker.github.io/p/ai-prompt-generator 這個網址是為了讓大家在使用 AI 繪畫的時候 ......

    uj5u.com 2023-04-20 07:43:36 more
  • 漫談前端自動化測驗演進之路及測驗工具分析

    隨著前端技術的不斷發展和應用程式的日益復雜,前端自動化測驗也在不斷演進。隨著 Web 應用程式變得越來越復雜,自動化測驗的需求也越來越高。如今,自動化測驗已經成為 Web 應用程式開發程序中不可或缺的一部分,它們可以幫助開發人員更快地發現和修復錯誤,提高應用程式的性能和可靠性。 ......

    uj5u.com 2023-04-20 07:43:16 more
  • CANN開發實踐:4個DVPP記憶體問題的典型案例解讀

    摘要:由于DVPP媒體資料處理功能對存放輸入、輸出資料的記憶體有更高的要求(例如,記憶體首地址128位元組對齊),因此需呼叫專用的記憶體申請介面,那么本期就分享幾個關于DVPP記憶體問題的典型案例,并給出原因分析及解決方法。 本文分享自華為云社區《FAQ_DVPP記憶體問題案例》,作者:昇騰CANN。 DVPP ......

    uj5u.com 2023-04-20 07:43:03 more
  • msf學習

    msf學習 以kali自帶的msf為例 一、msf核心模塊與功能 msf模塊都放在/usr/share/metasploit-framework/modules目錄下 1、auxiliary 輔助模塊,輔助滲透(埠掃描、登錄密碼爆破、漏洞驗證等) 2、encoders 編碼器模塊,主要包含各種編碼 ......

    uj5u.com 2023-04-20 07:42:59 more
  • Halcon軟體安裝與界面簡介

    1. 下載Halcon17版本到到本地 2. 雙擊安裝包后 3. 步驟如下 1.2 Halcon軟體安裝 界面分為四大塊 1. Halcon的五個助手 1) 影像采集助手:與相機連接,設定相機引數,采集影像 2) 標定助手:九點標定或是其它的標定,生成標定檔案及內參外參,可以將像素單位轉換為長度單位 ......

    uj5u.com 2023-04-20 07:42:17 more
  • 在MacOS下使用Unity3D開發游戲

    第一次發博客,先發一下我的游戲開發環境吧。 去年2月份買了一臺MacBookPro2021 M1pro(以下簡稱mbp),這一年來一直在用mbp開發游戲。我大致分享一下我的開發工具以及使用體驗。 1、Unity 官網鏈接: https://unity.cn/releases 我一般使用的Apple ......

    uj5u.com 2023-04-20 07:40:19 more