主頁 > 資料庫 > 面試題 - ThreadLocal詳解

面試題 - ThreadLocal詳解

2020-10-01 15:40:56 資料庫

目錄(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/shujuku/145590.html

標籤:其他

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

下一篇:ad域中巡檢統計資料用到的命令列

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

熱門瀏覽
  • GPU虛擬機創建時間深度優化

    **?桔妹導讀:**GPU虛擬機實體創建速度慢是公有云面臨的普遍問題,由于通常情況下創建虛擬機屬于低頻操作而未引起業界的重視,實際生產中還是存在對GPU實體創建時間有苛刻要求的業務場景。本文將介紹滴滴云在解決該問題時的思路、方法、并展示最終的優化成果。 從公有云服務商那里購買過虛擬主機的資深用戶,一 ......

    uj5u.com 2020-09-10 06:09:13 more
  • 可編程網卡芯片在滴滴云網路的應用實踐

    **?桔妹導讀:**隨著云規模不斷擴大以及業務層面對延遲、帶寬的要求越來越高,采用DPDK 加速網路報文處理的方式在橫向縱向擴展都出現了局限性。可編程芯片成為業界熱點。本文主要講述了可編程網卡芯片在滴滴云網路中的應用實踐,遇到的問題、帶來的收益以及開源社區貢獻。 #1. 資料中心面臨的問題 隨著滴滴 ......

    uj5u.com 2020-09-10 06:10:21 more
  • 滴滴資料通道服務演進之路

    **?桔妹導讀:**滴滴資料通道引擎承載著全公司的資料同步,為下游實時和離線場景提供了必不可少的源資料。隨著任務量的不斷增加,資料通道的整體架構也隨之發生改變。本文介紹了滴滴資料通道的發展歷程,遇到的問題以及今后的規劃。 #1. 背景 資料,對于任何一家互聯網公司來說都是非常重要的資產,公司的大資料 ......

    uj5u.com 2020-09-10 06:11:05 more
  • 滴滴AI Labs斬獲國際機器翻譯大賽中譯英方向世界第三

    **桔妹導讀:**深耕人工智能領域,致力于探索AI讓出行更美好的滴滴AI Labs再次斬獲國際大獎,這次獲獎的專案是什么呢?一起來看看詳細報道吧! 近日,由國際計算語言學協會ACL(The Association for Computational Linguistics)舉辦的世界最具影響力的機器 ......

    uj5u.com 2020-09-10 06:11:29 more
  • MPP (Massively Parallel Processing)大規模并行處理

    1、什么是mpp? MPP (Massively Parallel Processing),即大規模并行處理,在資料庫非共享集群中,每個節點都有獨立的磁盤存盤系統和記憶體系統,業務資料根據資料庫模型和應用特點劃分到各個節點上,每臺資料節點通過專用網路或者商業通用網路互相連接,彼此協同計算,作為整體提供 ......

    uj5u.com 2020-09-10 06:11:41 more
  • 滴滴資料倉庫指標體系建設實踐

    **桔妹導讀:**指標體系是什么?如何使用OSM模型和AARRR模型搭建指標體系?如何統一流程、規范化、工具化管理指標體系?本文會對建設的方法論結合滴滴資料指標體系建設實踐進行解答分析。 #1. 什么是指標體系 ##1.1 指標體系定義 指標體系是將零散單點的具有相互聯系的指標,系統化的組織起來,通 ......

    uj5u.com 2020-09-10 06:12:52 more
  • 單表千萬行資料庫 LIKE 搜索優化手記

    我們經常在資料庫中使用 LIKE 運算子來完成對資料的模糊搜索,LIKE 運算子用于在 WHERE 子句中搜索列中的指定模式。 如果需要查找客戶表中所有姓氏是“張”的資料,可以使用下面的 SQL 陳述句: SELECT * FROM Customer WHERE Name LIKE '張%' 如果需要 ......

    uj5u.com 2020-09-10 06:13:25 more
  • 滴滴Ceph分布式存盤系統優化之鎖優化

    **桔妹導讀:**Ceph是國際知名的開源分布式存盤系統,在工業界和學術界都有著重要的影響。Ceph的架構和演算法設計發表在國際系統領域頂級會議OSDI、SOSP、SC等上。Ceph社區得到Red Hat、SUSE、Intel等大公司的大力支持。Ceph是國際云計算領域應用最廣泛的開源分布式存盤系統, ......

    uj5u.com 2020-09-10 06:14:51 more
  • es~通過ElasticsearchTemplate進行聚合~嵌套聚合

    之前寫過《es~通過ElasticsearchTemplate進行聚合操作》的文章,這一次主要寫一個嵌套的聚合,例如先對sex集合,再對desc聚合,最后再對age求和,共三層嵌套。 Aggregations的部分特性類似于SQL語言中的group by,avg,sum等函式,Aggregation ......

    uj5u.com 2020-09-10 06:14:59 more
  • 爬蟲日志監控 -- Elastc Stack(ELK)部署

    傻瓜式部署,只需替換IP與用戶 導讀: 現ELK四大組件分別為:Elasticsearch(核心)、logstash(處理)、filebeat(采集)、kibana(可視化) 下載均在https://www.elastic.co/cn/downloads/下tar包,各組件版本最好一致,配合fdm會 ......

    uj5u.com 2020-09-10 06:15:05 more
最新发布
  • day02-2-商鋪查詢快取

    功能02-商鋪查詢快取 3.商鋪詳情快取查詢 3.1什么是快取? 快取就是資料交換的緩沖區(稱作Cache),是存盤資料的臨時地方,一般讀寫性能較高。 快取的作用: 降低后端負載 提高讀寫效率,降低回應時間 快取的成本: 資料一致性成本 代碼維護成本 運維成本 3.2需求說明 如下,當我們點擊商店詳 ......

    uj5u.com 2023-04-20 08:33:24 more
  • MySQL中binlog備份腳本分享

    關于MySQL的二進制日志(binlog),我們都知道二進制日志(binlog)非常重要,尤其當你需要point to point災難恢復的時侯,所以我們要對其進行備份。關于二進制日志(binlog)的備份,可以基于flush logs方式先切換binlog,然后拷貝&壓縮到到遠程服務器或本地服務器 ......

    uj5u.com 2023-04-20 08:28:06 more
  • day02-短信登錄

    功能實作02 2.功能01-短信登錄 2.1基于Session實作登錄 2.1.1思路分析 2.1.2代碼實作 2.1.2.1發送短信驗證碼 發送短信驗證碼: 發送驗證碼的介面為:http://127.0.0.1:8080/api/user/code?phone=xxxxx<手機號> 請求方式:PO ......

    uj5u.com 2023-04-20 08:27:27 more
  • 快取與資料庫雙寫一致性幾種策略分析

    本文將對幾種快取與資料庫保證資料一致性的使用方式進行分析。為保證高并發性能,以下分析場景不考慮執行的原子性及加鎖等強一致性要求的場景,僅追求最終一致性。 ......

    uj5u.com 2023-04-20 08:26:48 more
  • sql陳述句優化

    問題查找及措施 問題查找 需要找到具體的代碼,對其進行一對一優化,而非一直把關注點放在服務器和sql平臺 降低簡化每個事務中處理的問題,盡量不要讓一個事務拖太長的時間 例如檔案上傳時,應將檔案上傳這一步放在事務外面 微軟建議 4.啟動sql定時執行計劃 怎么啟動sqlserver代理服務-百度經驗 ......

    uj5u.com 2023-04-20 08:26:35 more
  • 云時代,MySQL到ClickHouse資料同步產品對比推薦

    ClickHouse 在執行分析查詢時的速度優勢很好的彌補了MySQL的不足,但是對于很多開發者和DBA來說,如何將MySQL穩定、高效、簡單的同步到 ClickHouse 卻很困難。本文對比了 NineData、MaterializeMySQL(ClickHouse自帶)、Bifrost 三款產品... ......

    uj5u.com 2023-04-20 08:26:29 more
  • sql陳述句優化

    問題查找及措施 問題查找 需要找到具體的代碼,對其進行一對一優化,而非一直把關注點放在服務器和sql平臺 降低簡化每個事務中處理的問題,盡量不要讓一個事務拖太長的時間 例如檔案上傳時,應將檔案上傳這一步放在事務外面 微軟建議 4.啟動sql定時執行計劃 怎么啟動sqlserver代理服務-百度經驗 ......

    uj5u.com 2023-04-20 08:25:13 more
  • Redis 報”OutOfDirectMemoryError“(堆外記憶體溢位)

    Redis 報錯“OutOfDirectMemoryError(堆外記憶體溢位) ”問題如下: 一、報錯資訊: 使用 Redis 的業務介面 ,產生 OutOfDirectMemoryError(堆外記憶體溢位),如圖: 格式化后的報錯資訊: { "timestamp": "2023-04-17 22: ......

    uj5u.com 2023-04-20 08:24:54 more
  • day02-2-商鋪查詢快取

    功能02-商鋪查詢快取 3.商鋪詳情快取查詢 3.1什么是快取? 快取就是資料交換的緩沖區(稱作Cache),是存盤資料的臨時地方,一般讀寫性能較高。 快取的作用: 降低后端負載 提高讀寫效率,降低回應時間 快取的成本: 資料一致性成本 代碼維護成本 運維成本 3.2需求說明 如下,當我們點擊商店詳 ......

    uj5u.com 2023-04-20 08:24:03 more
  • day02-短信登錄

    功能實作02 2.功能01-短信登錄 2.1基于Session實作登錄 2.1.1思路分析 2.1.2代碼實作 2.1.2.1發送短信驗證碼 發送短信驗證碼: 發送驗證碼的介面為:http://127.0.0.1:8080/api/user/code?phone=xxxxx<手機號> 請求方式:PO ......

    uj5u.com 2023-04-20 08:23:11 more