主頁 > 前端設計 > 面試題 - ThreadLocal詳解

面試題 - ThreadLocal詳解

2020-10-02 05:35:33 前端設計

目錄(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/qianduan/147379.html

標籤:其他

上一篇:[CTF-PWN]攻防世界新手村-when_did_you_born[wp]

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

標籤雲
其他(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)

熱門瀏覽
  • vue移動端上拉加載

    可能做得過于簡單或者比較low,請各位大佬留情,一起探討技術 ......

    uj5u.com 2020-09-10 04:38:07 more
  • 優美網站首頁,頂部多層導航

    一個個人用的瀏覽器首頁,可以把一下常用的網站放在這里,平常打開會比較方便。 第一步,HTML代碼 <script src=https://www.cnblogs.com/szharf/p/"js/jquery-3.4.1.min.js"></script> <div id="navigate"> <ul> <li class="labels labels_1"> ......

    uj5u.com 2020-09-10 04:38:47 more
  • 頁面為要加<!DOCTYPE html>

    最近因為寫一個js函式,需要用到$(window).height(); 由于手寫demo的時候,過于自信,其實對前端方面的認識也不夠體系,用文本檔案直接敲出來的html代碼,第一行沒有加上<!DOCTYPE html> 導致了$(window).height();的結果直接是整個document的高 ......

    uj5u.com 2020-09-10 04:38:52 more
  • WordPress網站程式手動升級要做好資料備份

    WordPress博客網站程式在進行升級前,必須要做好網站資料的備份,這個問題良家佐言是遇見過的;在剛開始接觸WordPress博客程式的時候,因為升級問題和博客網站的修改的一些嘗試,良家佐言是吃盡了苦頭。因為購買的是西部數碼的空間和域名,每當佐言把自己的WordPress博客網站搞到一塌糊涂的時候 ......

    uj5u.com 2020-09-10 04:39:30 more
  • WordPress程式不能升級為5.4.2版本的原因

    WordPress是一款個人博客系統,受到英文博客愛好者和中文博客愛好者的追捧,并逐步演化成一款內容管理系統軟體;它是使用PHP語言和MySQL資料庫開發的,用戶可以在支持PHP和MySQL資料庫的服務器上使用自己的博客。每一次WordPress程式的更新,就會牽動無數WordPress愛好者的心, ......

    uj5u.com 2020-09-10 04:39:49 more
  • 使用CSS3的偽元素進行首字母下沉和首行改變樣式

    網頁中常見的一種效果,首字改變樣式或者首行改變樣式,效果如下圖。 代碼: <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, ......

    uj5u.com 2020-09-10 04:40:09 more
  • 關于a標簽的講解

    什么是a標簽? <a> 標簽定義超鏈接,用于從一個頁面鏈接到另一個頁面。 <a> 元素最重要的屬性是 href 屬性,它指定鏈接的目標。 a標簽的語法格式:<a href=https://www.cnblogs.com/summerxbc/p/"指定要跳轉的目標界面的鏈接">需要展示給用戶看見的內容</a> a標簽 在所有瀏覽器中,鏈接的默認外觀如下: 未被訪問的鏈接帶 ......

    uj5u.com 2020-09-10 04:40:11 more
  • 前端輪播圖

    在需要輪播的頁面是引入swiper.min.js和swiper.min.css swiper.min.js地址: 鏈接:https://pan.baidu.com/s/15Uh516YHa4CV3X-RyjEIWw 提取碼:4aks swiper.min.css地址 鏈接:https://pan.b ......

    uj5u.com 2020-09-10 04:40:13 more
  • 如何設定html中的背景圖片(全屏顯示,且不拉伸)

    1 <style>2 body{background-image:url(https://uploadbeta.com/api/pictures/random/?key=BingEverydayWallpaperPicture); 3 background-size:cover;background ......

    uj5u.com 2020-09-10 04:40:16 more
  • Java學習——HTML詳解(上)

    HTML詳解 初識HTML Hyper Text Markup Language(超文本標記語言) 1 <!--DOCTYPE:告訴瀏覽器我們要使用什么規范--> 2 <!DOCTYPE html> 3 <html lang="en"> 4 <head> 5 <!--meta 描述性的標簽,描述一些 ......

    uj5u.com 2020-09-10 04:40:33 more
最新发布
  • 我的第一個NPM包:panghu-planebattle-esm(胖虎飛機大戰)使用說明

    好家伙,我的包終于開發完啦 歡迎使用胖虎的飛機大戰包!! 為你的主頁添加色彩 這是一個有趣的網頁小游戲包,使用canvas和js開發 使用ES6模塊化開發 效果圖如下: (覺得圖片太sb的可以自己改) 代碼已開源!! Git: https://gitee.com/tang-and-han-dynas ......

    uj5u.com 2023-04-20 07:59:23 more
  • 生產事故-走近科學之消失的JWT

    入職多年,面對生產環境,盡管都是小心翼翼,慎之又慎,還是難免捅出簍子。輕則滿頭大汗,面紅耳赤。重則系統停擺,損失資金。每一個生產事故的背后,都是寶貴的經驗和教訓,都是專案成員的血淚史。為了更好地防范和遏制今后的各類事故,特開此專題,長期更新和記錄大大小小的各類事故。有些是親身經歷,有些是經人耳傳口授 ......

    uj5u.com 2023-04-18 07:55:04 more
  • 記錄--Canvas實作打飛字游戲

    這里給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 打開游戲界面,看到一個畫面簡潔、卻又富有挑戰性的游戲。螢屏上,有一個白色的矩形框,里面不斷下落著各種單詞,而我需要迅速地輸入這些單詞。如果我輸入的單詞與螢屏上的單詞匹配,那么我就可以獲得得分;如果我輸入的單詞錯誤或者時間過長,那么我就會輸 ......

    uj5u.com 2023-04-04 08:35:30 more
  • 了解 HTTP 看這一篇就夠

    在學習網路之前,了解它的歷史能夠幫助我們明白為何它會發展為如今這個樣子,引發探究網路的興趣。下面的這張圖片就展示了“互聯網”誕生至今的發展歷程。 ......

    uj5u.com 2023-03-16 11:00:15 more
  • 藍牙-低功耗中心設備

    //11.開啟藍牙配接器 openBluetoothAdapter //21.開始搜索藍牙設備 startBluetoothDevicesDiscovery //31.開啟監聽搜索藍牙設備 onBluetoothDeviceFound //30.停止監聽搜索藍牙設備 offBluetoothDevi ......

    uj5u.com 2023-03-15 09:06:45 more
  • canvas畫板(滑鼠和觸摸)

    <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>canves</title> <style> #canvas { cursor:url(../images/pen.png),crosshair; } #canvasdiv{ bo ......

    uj5u.com 2023-02-15 08:56:31 more
  • 手機端H5 實作自定義拍照界面

    手機端 H5 實作自定義拍照界面也可以使用 MediaDevices API 和 <video> 標簽來實作,和在桌面端做法基本一致。 首先,使用 MediaDevices.getUserMedia() 方法獲取攝像頭媒體流,并將其傳遞給 <video> 標簽進行渲染。 接著,使用 HTML 的 < ......

    uj5u.com 2023-01-12 07:58:22 more
  • 記錄--短視頻滑動播放在 H5 下的實作

    這里給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 短視頻已經無數不在了,但是主體還是使用 app 來承載的。本文講述 H5 如何實作 app 的視頻滑動體驗。 無聲勝有聲,一圖頂百辯,且看下圖: 網址鏈接(需在微信或者手Q中瀏覽) 從上圖可以看到,我們主要實作的功能也是本文要講解的有: ......

    uj5u.com 2023-01-04 07:29:05 more
  • 一文讀懂 HTTP/1 HTTP/2 HTTP/3

    從 1989 年萬維網(www)誕生,HTTP(HyperText Transfer Protocol)經歷了眾多版本迭代,WebSocket 也在期間萌芽。1991 年 HTTP0.9 被發明。1996 年出現了 HTTP1.0。2015 年 HTTP2 正式發布。2020 年 HTTP3 或能正... ......

    uj5u.com 2022-12-24 06:56:02 more
  • 【HTML基礎篇002】HTML之form表單超詳解

    ??一、form表單是什么

    ??二、form表單的屬性

    ??三、input中的各種Type屬性值

    ??四、標簽 ......

    uj5u.com 2022-12-18 07:17:06 more