ThreadLocal(史上最全)

文章很長,建議收藏起來,慢慢讀! 瘋狂創客圈爲小夥伴奉上如下珍貴的學習資源:html


推薦2:史上最全 Java 面試題 21 個專題

史上最全 Java 面試題 21 個專題 阿里、京東、美團、頭條.... 隨意挑、橫着走!!!
1: JVM面試題(史上最強、持續更新、吐血推薦) http://www.javashuo.com/article/p-vhnpdnhb-vd.html
2:Java基礎面試題(史上最全、持續更新、吐血推薦) http://www.javashuo.com/article/p-otujhkjp-vd.html
3:死鎖面試題(史上最強、持續更新) http://www.javashuo.com/article/p-uyudvdol-vd.html
4:設計模式面試題 (史上最全、持續更新、吐血推薦) http://www.javashuo.com/article/p-qnkzhtsu-vd.html
5:架構設計面試題 (史上最全、持續更新、吐血推薦) http://www.javashuo.com/article/p-dlpjqbmg-vd.html
還有 10 +必刷、必刷 的面試題 更多 ....., 請參見【 瘋狂創客圈 高併發 總目錄

推薦3: 瘋狂創客圈 高質量 博文

springCloud 高質量 博文
nacos 實戰(史上最全) sentinel (史上最全+入門教程)
springcloud + webflux 高併發實戰 Webflux(史上最全)
SpringCloud gateway (史上最全) spring security (史上最全)
還有 10 +必刷、必刷 的高質量 博文 更多 ....., 請參見【 瘋狂創客圈 高併發 總目錄

1、ThreadLocal 介紹:

正如 JDK 註釋中所說的那樣: ThreadLocal 類提供線程局部變量,它一般是私有類中但願將狀態與線程關聯的靜態字段。java

簡而言之,就是 ThreadLocal 提供了線程間數據隔離的功能,從它的命名上也能知道這是屬於一個線程的本地變量。也就是說,每一個線程都會在 ThreadLocal 中保存一份該線程獨有的數據,因此它是線程安全的。程序員

熟悉 Spring 的同窗可能知道 Bean 的做用域(Scope),而 ThreadLocal 的做用域就是線程。web

下面經過一個簡單示例來展現一下 ThreadLocal 的特性:面試

public static void main(String[] args) {
  ThreadLocal<String> threadLocal = new ThreadLocal<>();
  // 建立一個有2個核心線程數的線程池
  ExecutorService threadPool = new ThreadPoolExecutor(2, 2, 1, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(10));
  // 線程池提交一個任務,將任務序號及執行該任務的子線程的線程名放到 ThreadLocal 中
  threadPool.execute(() -> threadLocal.set("任務1: " + Thread.currentThread().getName()));
  threadPool.execute(() -> threadLocal.set("任務2: " + Thread.currentThread().getName()));
  threadPool.execute(() -> threadLocal.set("任務3: " + Thread.currentThread().getName()));
  // 輸出 ThreadLocal 中的內容
  for (int i = 0; i < 10; i++) {
    threadPool.execute(() -> System.out.println("ThreadLocal value of " + Thread.currentThread().getName() + " = " + threadLocal.get()));
  }
  // 線程池記得關閉
  threadPool.shutdown();
}

上面代碼首先建立了一個有2個核心線程數的普通線程池,隨後提交一個任務,將任務序號及執行該任務的子線程的線程名放到 ThreadLocal 中,最後在一個 for 循環中輸出線程池中各個線程存儲在 ThreadLocal 中的值。算法

這個程序的輸出結果是:spring

ThreadLocal value of pool-1-thread-1 = 任務3: pool-1-thread-1
ThreadLocal value of pool-1-thread-2 = 任務2: pool-1-thread-2
ThreadLocal value of pool-1-thread-1 = 任務3: pool-1-thread-1
ThreadLocal value of pool-1-thread-2 = 任務2: pool-1-thread-2
ThreadLocal value of pool-1-thread-1 = 任務3: pool-1-thread-1
ThreadLocal value of pool-1-thread-2 = 任務2: pool-1-thread-2
ThreadLocal value of pool-1-thread-1 = 任務3: pool-1-thread-1
ThreadLocal value of pool-1-thread-2 = 任務2: pool-1-thread-2
ThreadLocal value of pool-1-thread-1 = 任務3: pool-1-thread-1
ThreadLocal value of pool-1-thread-2 = 任務2: pool-1-thread-2

因而可知,線程池中執行提交的任務的是名爲 pool-1-thread-1 的線程,隨後屢次輸出線程池核心線程在 ThreadLocal 變量中存儲的的內容也代表:每一個線程在 ThreadLocal 中存儲的內容是當前線程獨有的,在多線程環境下,可以有效防止本身的變量被其餘線程修改(存儲的內容是同一個引用類型對象的狀況除外)。編程

2、ThreadLocal 實現原理:

在 JDK1.8 版本中 ThreadLocal 類的源碼總共723行,去掉註釋大概有350行,應該算是 JDK 核心類庫中代碼量比較少的一個類了,相對來講它的源碼仍是挺容易理解的。設計模式

下面,就從 ThreadLocal 的數據結構開始聊聊它的實現原理吧。數組

底層數據結構:

ThreadLocal 底層是經過 ThreadLocalMap 這個靜態內部類來存儲數據的,ThreadLocalMap 就是一個鍵值對的 Map,它的底層是 Entry 對象數組,Entry 對象中存放的鍵是 ThreadLocal 對象,值是 Object 類型的具體存儲內容。

除此以外,ThreadLocalMap 也是 Thread 類一個屬性。

img

如何證實上面給出的 ThreadLocal 類底層數據結構的正確性?

咱們能夠從 ThreadLocal#get() 方法開始追蹤代碼,看看線程局部變量究竟是從哪裏被取出來的。

public T get() {
  // 獲取當前線程
  Thread t = Thread.currentThread();
  // 獲取 Thread 類中 ThreadLocal.ThreadLocalMap 類型的 threadLocals 變量
  ThreadLocalMap map = getMap(t);
  // 若 threadLocals 變量不爲空,根據 ThreadLocal 對象來獲取 key 對應的 value
  if (map != null) {
    ThreadLocalMap.Entry e = map.getEntry(this);
    if (e != null) {
      @SuppressWarnings("unchecked")
      T result = (T)e.value;
      return result;
    }
  }
  // 若 threadLocals 變量是 NULL,初始化一個新的 ThreadLocalMap 對象
  return setInitialValue();
}

// ThreadLocal#setInitialValue
// 初始化一個新的 ThreadLocalMap 對象
private T setInitialValue() {
  // 初始化一個 NULL 值
  T value = initialValue();
  // 獲取當前線程
  Thread t = Thread.currentThread();
  // 獲取 Thread 類中 ThreadLocal.ThreadLocalMap 類型的 threadLocals 變量
  ThreadLocalMap map = getMap(t);
  if (map != null)
    map.set(this, value);
  else
    createMap(t, value);
  return value;
}

// ThreadLocalMap#createMap
void createMap(Thread t, T firstValue) {
  t.threadLocals = new ThreadLocalMap(this, firstValue);
}

經過 ThreadLocal#get() 方法能夠很清晰的看到,咱們根據 ThreadLocal 對象從 ThreadLocal 中讀取數據時,首先會獲取當前線程對象,而後獲得當前線程對象中 ThreadLocal.ThreadLocalMap 類型的 threadLocals 屬性;

若是 threadLocals 屬性不爲空,會根據 ThreadLocal 對象做爲 key 來獲取 key 對應的 value;若是 threadLocals 變量是 NULL,就初始化一個新的ThreadLocalMap 對象。

再看 ThreadLocalMap 的構造方法,也就是 Thread 類中 ThreadLocal.ThreadLocalMap 類型的 threadLocals 屬性不爲空時的執行邏輯。

// ThreadLocalMap 構造方法
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
  table = new Entry[INITIAL_CAPACITY];
  int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
  table[i] = new Entry(firstKey, firstValue);
  size = 1;
  setThreshold(INITIAL_CAPACITY);
}

這個構造方法實際上是將 ThreadLocal 對象做爲 key,存儲的具體內容 Object 對象做爲 value,包裝成一個 Entry 對象,放到 ThreadLocalMap 類中類型爲 Entry 數組的 table 屬性中,這樣就完成了線程局部變量的存儲。

因此說, ThreadLocal 中的數據最終是存放在 ThreadLocalMap 這個類中的

散列方式:

ThreadLocalMap#set(ThreadLocal<?> key, Object value) 方法中我寫了一行註釋:

// 獲取當前 ThreadLocal 對象的散列值
int i = key.threadLocalHashCode & (len-1);

這行代碼獲得的值實際上是一個 ThreadLocal 對象的散列值,這就是 ThreadLocal 的散列方式,咱們稱之爲 斐波那契散列

// ThreadLocal#threadLocalHashCode
private final int threadLocalHashCode = nextHashCode();

// ThreadLocal#nextHashCode
private static int nextHashCode() {
	  return nextHashCode.getAndAdd(HASH_INCREMENT);
}

// ThreadLocal#nextHashCode
private static AtomicInteger nextHashCode = new AtomicInteger();

// AtomicInteger#getAndAdd
public final int getAndAdd(int delta) {
  	return unsafe.getAndAddInt(this, valueOffset, delta);
}

// 魔數 ThreadLocal#HASH_INCREMENT
private static final int HASH_INCREMENT = 0x61c88647;

key.threadLocalHashCode 所涉及的函數及屬性如上所示,每個 ThreadLocal 的 threadLocalHashCode 屬性都是基於魔數 0x61c88647 來生成的。

這裏就不討論選擇這個魔數的緣由了(實際上是我看不太懂),總之大量的實踐證實: 使用 0x61c88647 做爲魔數生成的 threadLocalHashCode 再與2的冪取餘,獲得的結果分佈很均勻。

注: 對 A 進行2的冪取餘操做 A % 2^N 能夠經過 A & (2^n-1) 來代替,位運算的效率比取模效率高不少。

如何解決哈希衝突:

咱們已經知道 ThreadLocalMap 類的底層數據結構是一個 Entry 類型的數組,但與 HashMap 中的 Node 類數組+鏈表形式不一樣的是,Entry 類沒有 next 屬性來構成鏈表,因此它是一個單純的數組。

就算上面所說的 斐波那契散列法 真的可以充分散列,但不免仍是可能會發生哈希碰撞,那麼問題來了,Entry 數組是如何解決哈希衝突的?

這就須要拿出 ThreadLocal#set(T value) 方法了,而具體處理哈希衝突的邏輯是在 ThreadLocalMap#set(ThreadLocal<?> key, Object value) 方法中的:

public void set(T value) {
  // 獲取當前線程
  Thread t = Thread.currentThread();
  // 獲取 Thread 類中 ThreadLocal.ThreadLocalMap 類型的 threadLocals 變量
  ThreadLocalMap map = getMap(t);
  // 若 threadLocals 變量不爲空,進行賦值;不然新建一個 ThreadLocalMap 對象來存儲
  if (map != null)
    map.set(this, value);
  else
    createMap(t, value);
}

// ThreadLocalMap#set
private void set(ThreadLocal<?> key, Object value) {
  // 獲取 ThreadLocalMap 的 Entry 數組對象
  Entry[] tab = table;
  int len = tab.length;
  // 基於斐波那契散列法獲取當前 ThreadLocal 對象的散列值
  int i = key.threadLocalHashCode & (len-1);
  // 解決哈希衝突,線性探測法
  for (Entry e = tab[i];
       e != null;
       e = tab[i = nextIndex(i, len)]) {
    ThreadLocal<?> k = e.get();
		// 代碼(1)
    if (k == key) {
      e.value = value;
      return;
    }
		// 代碼(2)
    if (k == null) {
      replaceStaleEntry(key, value, i);
      return;
    }
  }
  // 代碼(3)將 key-value 包裝成 Entry 對象放在數組退出循環時的位置中
  tab[i] = new Entry(key, value);
  int sz = ++size;
  if (!cleanSomeSlots(i, sz) && sz >= threshold)
    rehash();
}

// ThreadLocalMap#nextIndex
// Entry 數組的下一個索引,若超過數組大小則從0開始,至關於環形數組
private static int nextIndex(int i, int len) {
  return ((i + 1 < len) ? i + 1 : 0);
}

具體分析處理哈希衝突的 ThreadLocalMap#set(ThreadLocal<?> key, Object value) 方法,能夠看到,在拿到 ThreadLocal 對象的散列值以後進入了一個 for 循環,循環的條件也很清楚:從 Entry 數組的 ThreadLocal 對象散列值處開始,每次向後挪一位,若是超過數組大小則從0開始繼續遍歷,直到 Entry 對象爲 NULL 爲止。

在循環過程當中:

  • 如代碼(1),若是當前 ThreadLocal 對象正好等於 Entry 對象中的 key 屬性,直接更新 ThreadLocal 中 value 的值;
  • 如代碼(2),若是當前 ThreadLocal 對象不等於 Entry 對象中的 key 屬性,而且 Entry 對象的 key 是空的,這裏進行的邏輯實際上是 設置鍵值對,同時清理無效的 Entry (必定程序防止內存泄漏,下文會有詳細介紹);
  • 如代碼(3),若是在遍歷中沒有發現當前 TheadLocal 對象的散列值,也沒有發現 Entry 對象的 key 爲空的狀況,而是知足了退出循環的條件,即 Entry 對象爲空時,那麼就會建立一個 新的 Entry 對象進行存儲 ,同時作一次 啓發式清理 ,將 Entry 數組中 key 爲空,value 不爲空的對象的 value 值釋放;

至此,咱們分析完了在向 ThreadLocal 中存儲數據時,拿到 ThreadLocal 對象散列值以後的邏輯,回到本小節的主題—— ThreadLocal 是如何解決哈希衝突的?

由上面的代碼能夠知道,在基於斐波那契散列法獲取當前 ThreadLocal 對象的散列值以後進入了一個循環,在循環中是處理具體處理哈希衝突的方法:

  • 若是散列值已存在且 key 爲同一個對象,直接更新 value
  • 若是散列值已存在但 key 不是同一個對象,嘗試在下一個空的位置進行存儲

因此,來總結一下 ThreadLocal 處理哈希衝突的方式就是:若是在 set 時遇到哈希衝突,ThreadLocal 會經過線性探測法嘗試在數組下一個索引位置進行存儲,同時在 set 過程當中 ThreadLocal 會釋放 key 爲 NULL,value 不爲 NULL 的髒 Entry對象的 value 屬性來防止內存泄漏

初始容量及擴容機制:

在上文中有提到過 ThreadLocalMap 的構造方法,這裏詳細說明一下。

// ThreadLocalMap 構造方法
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
  // 初始化 Entry 數組
  table = new Entry[INITIAL_CAPACITY];
  int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
  table[i] = new Entry(firstKey, firstValue);
  size = 1;
  // 設置擴容條件
  setThreshold(INITIAL_CAPACITY);
}

ThreadLocalMap 的初始容量是 16:

// 初始化容量
private static final int INITIAL_CAPACITY = 16;

下面聊一下 ThreadLocalMap 的擴容機制 ,它在擴容前有兩個判斷的步驟,都知足後纔會進行最終擴容:

  • ThreadLocalMap#set(ThreadLocal<?> key, Object value) 方法中可能會觸發啓發式清理,在清理無效 Entry 對象後,若是數組長度大於等於數組定義長度的 2/3,則首先進行 rehash;
// rehash 條件
private void setThreshold(int len) {
  threshold = len * 2 / 3;
}
  • rehash 會觸發一次全量清理,若是數組長度大於等於數組定義長度的 1/2,則進行 resize(擴容);
// 擴容條件
private void rehash() {
  expungeStaleEntries();

  // Use lower threshold for doubling to avoid hysteresis
  if (size >= threshold - threshold / 4)
    resize();
}
  • 進行擴容時,Entry 數組爲擴容爲 原來的2倍 ,從新計算 key 的散列值,若是遇到 key 爲 NULL 的狀況,會將其 value 也置爲 NULL,幫助虛擬機進行GC。
// 具體的擴容函數
private void resize() {
  Entry[] oldTab = table;
  int oldLen = oldTab.length;
  int newLen = oldLen * 2;
  Entry[] newTab = new Entry[newLen];
  int count = 0;

  for (int j = 0; j < oldLen; ++j) {
    Entry e = oldTab[j];
    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;
}

父子線程間局部變量如何傳遞:

咱們已經知道 ThreadLocal 中存儲的是線程的局部變量,那若是如今有個需求,想要實現線程間局部變量傳遞,這該如何實現呢?

大佬們早已料到會有這樣的需求,因而設計出了 InheritableThreadLocal 類。

InheritableThreadLocal 類的源碼除去註釋以外一共不超過10行,由於它是繼承於 ThreadLocal 類,不少東西在 ThreadLocal 類中已經實現了,InheritableThreadLocal 類只重寫了其中三個方法:

public class InheritableThreadLocal<T> extends ThreadLocal<T> {

    protected T childValue(T parentValue) {
        return parentValue;
    }

    ThreadLocalMap getMap(Thread t) {
       return t.inheritableThreadLocals;
    }
  
    void createMap(Thread t, T firstValue) {
        t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
    }
}

咱們先用一個簡單的示例來實踐一下父子線程間局部變量的傳遞功能。

public static void main(String[] args) {
  ThreadLocal<String> threadLocal = new InheritableThreadLocal<>();
  threadLocal.set("這是父線程設置的值");

  new Thread(() -> System.out.println("子線程輸出:" + threadLocal.get())).start();
}

// 輸出內容
子線程輸出:這是父線程設置的值

能夠看到,在子線程中經過調用 InheritableThreadLocal#get() 方法,拿到了在父線程中設置的值。

那麼,這是如何實現的呢?

實現父子線程間的局部變量共享須要追溯到 Thread 對象的構造方法:

public Thread(Runnable target) {
  init(null, target, "Thread-" + nextThreadNum(), 0);
}

private void init(ThreadGroup g, Runnable target, String name, long stackSize) {
  init(g, target, name, stackSize, null, true);
}

private void init(ThreadGroup g, Runnable target, String name,
                  long stackSize, AccessControlContext acc,
                  // 該參數通常默認是 true
                  boolean inheritThreadLocals) {
  // 省略大部分代碼
  Thread parent = currentThread();
  
  // 複製父線程的 inheritableThreadLocals 屬性,實現父子線程局部變量共享
  if (inheritThreadLocals && parent.inheritableThreadLocals != null) {
   	this.inheritableThreadLocals =
    ThreadLocal.createInheritedMap(parent.inheritableThreadLocals); 
  }
  
	// 省略部分代碼
}

在最終執行的構造方法中,有這樣一個判斷:若是當前父線程(建立子線程的線程)的 inheritableThreadLocals 屬性不爲 NULL,就會將當下父線程的 inheritableThreadLocals 屬性複製給子線程的 inheritableThreadLocals 屬性。具體的複製方法以下:

// ThreadLocal#createInheritedMap
static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
  return new ThreadLocalMap(parentMap);
}

private ThreadLocalMap(ThreadLocalMap parentMap) {
  Entry[] parentTable = parentMap.table;
  int len = parentTable.length;
  setThreshold(len);
  table = new Entry[len];
	// 一個個複製父線程 ThreadLocalMap 中的數據
  for (int j = 0; j < len; j++) {
    Entry e = parentTable[j];
    if (e != null) {
      @SuppressWarnings("unchecked")
      ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
      if (key != null) {
        // childValue 方法調用的是 InheritableThreadLocal#childValue(T parentValue)
        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++;
      }
    }
  }
}

須要注意的是,複製父線程共享變量的時機是在建立子線程時,若是在建立子線程後父線程再往 InheritableThreadLocal 類型的對象中設置內容,將再也不對子線程可見。

ThreadLocal 內存泄漏分析:

最後再來講說 ThreadLocal 的內存泄漏問題,衆所周知,若是使用不當,ThreadLocal 會致使內存泄漏。

內存泄漏 是指程序中已動態分配的堆內存因爲某種緣由程序未釋放或沒法釋放,形成系統內存的浪費,致使程序運行速度減慢甚至系統崩潰等嚴重後果。

發生內存泄漏的緣由:

而 ThreadLocal 發生內存泄漏的緣由須要從 Entry 對象提及。

// ThreadLocal->ThreadLocalMap->Entry
static class Entry extends WeakReference<ThreadLocal<?>> {
  /** The value associated with this ThreadLocal. */
  Object value;

  Entry(ThreadLocal<?> k, Object v) {
    super(k);
    value = v;
  }
}

Entry 對象的 key 即 ThreadLocal 類是繼承於 WeakReference 弱引用類。具備弱引用的對象有更短暫的生命週期,在發生 GC 活動時,不管內存空間是否足夠,垃圾回收器都會回收具備弱引用的對象。

因爲 Entry 對象的 key 是繼承於 WeakReference 弱引用類的,若 ThreadLocal 類沒有外部強引用,當發生 GC 活動時就會將 ThreadLocal 對象回收。

而此時若是建立 ThreadLocal 類的線程依然活動,那麼 Entry 對象中 ThreadLocal 對象對應的 value 就依舊具備強引用而不會被回收,從而致使內存泄漏。

如何解決內存泄漏問題:

要想解決內存泄漏問題其實很簡單,只須要記得在使用完 ThreadLocal 中存儲的內容後將它 remove 掉就能夠了。

這是主動防止發生內存泄漏問題的手段,但其實設計 ThreadLocal 的大神固然也發現了 ThreadLocal 可能引起內存泄漏的問題,因此他們也設計了相應的手段來防止內存泄漏。

ThreadLocal 內部如何防止內存泄漏:

在上文中描述 ThreadLocalMap#set(ThreadLocal key, Object value) 其實已經有涉及 ThreadLocal 內部清理無效 Entry 的邏輯了,在經過線性檢測法處理哈希衝突時,若 Entry 數組的 key 與當前 ThreadLocal 不是同一個對象,同時 key 爲空的時候,會進行 清理無效 Entry 的處理,即 ThreadLOcalMap#replaceStaleEntry(ThreadLocal key, Object value, int staleSlot) 方法:

  • 這個方法中也是一個循環,循環的邏輯與 ThreadLocalMap#set(ThreadLocal<?> key, Object value) 方法一致;
  • 在循環過程當中若是找到了將要存儲的 ThreadLocal 對象,則會將它與進入 replaceStaleEntry 方法時知足條件的 k 值作交換,同時將 value 更新;
  • 若是沒有找到將要存儲的 ThreadLocal 對象,則會在此 k 值處新建一個 Entry 對象存儲;
  • 同時,在循環過程當中若是發現其餘無效的 Entry( key 爲 NULL,value還在的狀況,可能致使內存泄漏,下文會有詳細描述),會順勢找到 Entry 數組中全部的無效 Entry,釋放這些無效 Entry(經過將 key 和 value 都設置爲NULL),在必定程度上避免了內存泄漏;

若是知足線性檢測循環結束條件了,即遇到了 Entry==NULL 的狀況,就新建一個 Entry 對象來存儲數據。而後會進行一次啓發式清理,若是啓發式清理沒有成功釋放知足條件的對象,同時知足擴容條件時,會執行 ThreadLocalMap#rehash() 方法。

private void rehash() {
  // 全量清理
  expungeStaleEntries();
  // 知足條件則擴容
  if (size >= threshold - threshold / 4)
    resize();
}

ThreadLocalMap#rehash() 方法中會對 ThreadLocalMap 進行一次全量清理,全量清理會遍歷整個 Entry 數組,刪除全部 key 爲 NULL,value 不爲 NULL 的髒 Entry對象。

// 全量清理
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);
  }
}

進行全量清理以後,若是 Entry 數組的大小大於等於 threshold - threshold / 4 ,則會進行2倍擴容。

總結一下:在ThreadLocal 內部是經過在 get、set、remove 方法中主動進行清理 key 爲 NULL 且 value 不爲 NULL 的無效 Entry 來避免內存泄漏問題。

可是基於 get、set 方法讓 ThreadLocal 自行清理無效 Entry 對象並不能徹底避免內存泄漏問題,要完全解決內存泄漏問題還得養成使用完就主動調用 remove 方法釋放資源的好習慣。

3、ThreadLocal的常見面試題目

什麼是ThreadLocal

ThreadLocal 是 JDK java.lang 包下的一個類,是自然的線程安全的類,

1.ThreadLoca 是線程局部變量,這個變量與普通變量的區別,在於每一個訪問該變量的線程,在線程內部都會
初始化一個獨立的變量副本,只有該線程能夠訪問【get() or set()】該變量,ThreadLocal實例一般聲明
爲 private static。

2.線程在存活而且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)與線程關聯起來。

ThreadLocalMap 和HashMap區別

HashMap 的數據結構是數組+鏈表

ThreadLocalMap的數據結構僅僅是數組

HashMap 是經過鏈地址法解決hash 衝突的問題

ThreadLocalMap 是經過開放地址法來解決hash 衝突的問題

HashMap 裏面的Entry 內部類的引用都是強引用

ThreadLocalMap裏面的Entry 內部類中的key 是弱引用,value 是強引用

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類型的成員變量,所以它是一個橋樑,自己不具有存儲功能

鏈地址法

這種方法的基本思想是將全部哈希地址爲i的元素構成一個稱爲同義詞鏈的單鏈表,並將單鏈表的頭指針存在哈希表的第i個單元中,於是查找、插入和刪除主要在同義詞鏈中進行。列如對於關鍵字集合{12,67,56,16,25,37, 22,29,15,47,48,34},咱們用前面一樣的12爲除數,進行除留餘數法:

在這裏插入圖片描述

開放地址法

這種方法的基本思想是一旦發生了衝突,就去尋找下一個空的散列地址(這很是重要,源碼都是根據這個特性,必須理解這裏才能往下走),只要散列表足夠大,空的散列地址總能找到,並將記錄存入。

好比說,咱們的關鍵字集合爲{12,33,4,5,15,25},表長爲10。 咱們用散列函數f(key) = key mod l0。 當計算前S個數{12,33,4,5}時,都是沒有衝突的散列地址,直接存入(藍色表明爲空的,能夠存放數據):

在這裏插入圖片描述

計算key = 15時,發現f(15) = 5,此時就與5所在的位置衝突。因而咱們應用上面的公式f(15) = (f(15)+1) mod 10 =6。因而將15存入下標爲6的位置。這其實就是房子被人買了因而買下一間的做法:

在這裏插入圖片描述

鏈地址法和開放地址法的優缺點

開放地址法:

容易產生堆積問題,不適於大規模的數據存儲。

散列函數的設計對衝突會有很大的影響,插入時可能會出現屢次衝突的現象。

刪除的元素是多個衝突元素中的一個,須要對後面的元素做處理,實現較複雜。

鏈地址法:

處理衝突簡單,且無堆積現象,平均查找長度短。

鏈表中的結點是動態申請的,適合構造表不能肯定長度的狀況。

刪除結點的操做易於實現。只要簡單地刪去鏈表上相應的結點便可。

指針須要額外的空間,故當結點規模較小時,開放定址法較爲節省空間。

ThreadLocalMap 採用開放地址法緣由

ThreadLocal 中看到一個屬性 HASH_INCREMENT = 0x61c88647 ,0x61c88647 是一個神奇的數字,讓哈希碼能均勻的分佈在2的N次方的數組裏, 即 Entry[] table

經過HASH_INCREMENT 能夠看到,ThreadLocal 中使用了斐波那契散列法,來保證哈希表的離散度。而它選用的乘數值便是2^32 * 黃金分割比

什麼是散列?

散列(Hash)也稱爲哈希,就是把任意長度的輸入,經過散列算法,變換成固定長度的輸出,這個輸出值就是散列值。

ThreadLocal 每每存放的數據量不會特別大(並且key 是弱引用又會被垃圾回收,及時讓數據量更小),這個時候開放地址法簡單的結構會顯得更省空間,同時數組的查詢效率也是很是高,加上第一點的保障,衝突機率也低.

解決哈希衝突

ThreadLocal中的hash code很是簡單,就是調用AtomicInteger的getAndAdd方法,參數是個固定值0x61c88647。

private static AtomicInteger nextHashCode =
    new AtomicInteger();
private static int nextHashCode() {
    return nextHashCode.getAndAdd(HASH_INCREMENT);
}

上面說過ThreadLocalMap的結構很是簡單隻用一個數組存儲,並無鏈表結構,當出現Hash衝突時採用線性查找的方式,所謂線性查找,就是根據初始key的hashcode值肯定元素在table數組中的位置,若是發現這個位置上已經有其餘key值的元素被佔用,則利用固定的算法尋找必定步長的下個位置,依次判斷,直至找到可以存放的位置。若是產生屢次hash衝突,處理起來就沒有HashMap的效率高,爲了不哈希衝突,使用盡可能少的threadlocal變量

內存泄漏問題

在JAVA裏面,存在強引用、弱引用、軟引用、虛引用。這裏主要談一下強引用和弱引用。

強引用,就沒必要說了,相似於:

A a = new A();

B b = new B();

考慮這樣的狀況:

C c = new C(b);

b = null;

考慮下GC的狀況。要知道b被置爲null,那麼是否意味着一段時間後GC工做能夠回收b所分配的內存空間呢?答案是否認的,由於即使b被置爲null,可是c仍然持有對b的引用,並且仍是強引用,因此GC不會回收b原先所分配的空間!既不能回收利用,又不能使用,這就形成了內存泄露

那麼如何處理呢?

能夠c = null;也可使用弱引用!(WeakReference w = new WeakReference(b);)

ThreadLocal使用到了弱引用,是否意味着不會存在內存泄露呢?

把ThreadLocal置爲null,那麼意味着Heap中的ThreadLocal實例不在有強引用指向,只有弱引用存在,所以GC是能夠回收這部分空間的,也就是key是能夠回收的。可是value卻存在一條從Current Thread過來的強引用鏈。所以只有當Current Thread銷燬時,value才能獲得釋放。

只要這個線程對象被gc回收,就不會出現內存泄露,但在threadLocal設爲null和線程結束這段時間內不會被回收的,就發生了咱們認爲的內存泄露。最要命的是線程對象不被回收的狀況,好比使用線程池的時候,線程結束是不會銷燬的,再次使用的,就可能出現內存泄露。

那麼如何有效的避免呢?

在ThreadLocalMap中的set/getEntry方法中,會對key爲null(也便是ThreadLocal爲null)進行判斷,若是爲null的話,那麼是會對value置爲null的。咱們也能夠經過調用ThreadLocal的remove方法進行釋放!也就是每次使用完ThreadLocal,都調用它的remove()方法,清除數據。

ThreadLocal使用

ThreadLocal使用的通常步驟:

一、在多線程的類(如ThreadDemo類)中。建立一個ThreadLocal對象threadXxx,用來保存線程間需要隔離處理的對象xxx。
二、在ThreadDemo類中。建立一個獲取要隔離訪問的數據的方法getXxx(),在方法中推斷,若ThreadLocal對象爲null時候,應該new()一個隔離訪問類型的對象,並強制轉換爲要應用的類型。
三、在ThreadDemo類的run()方法中。經過getXxx()方法獲取要操做的數據。這樣可以保證每個線程相應一個數據對象,在不論什麼時刻都操做的是這個對象。

使用示例:

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

    public static void main(String[] args) {

        new Thread(() -> {
            try {
                for (int i = 0; i < 10; i++) {
                    threadLocal.set(i);
                    System.out.println(Thread.currentThread().getName() + " = " + threadLocal.get());
                    try {
                        Thread.sleep(200);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            } finally {
                threadLocal.remove();
            }
        }, "threadLocal test 1").start();


        new Thread(() -> {
            try {
                for (int i = 0; i < 10; i++) {
                    System.out.println(Thread.currentThread().getName() + " = " + threadLocal.get());
                    try {
                        Thread.sleep(200);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            } finally {
                threadLocal.remove();
            }
        }, "threadLocal test 2").start();
    }

輸出

threadLocal test 1 = 0
threadLocal test 2 = null
threadLocal test 2 = null
threadLocal test 1 = 1
threadLocal test 2 = null
threadLocal test 1 = 2
threadLocal test 2 = null
threadLocal test 1 = 3
threadLocal test 2 = null
threadLocal test 1 = 4
threadLocal test 2 = null
threadLocal test 1 = 5
threadLocal test 2 = null
threadLocal test 1 = 6
threadLocal test 2 = null
threadLocal test 1 = 7
threadLocal test 2 = null
threadLocal test 1 = 8
threadLocal test 2 = null
threadLocal test 1 = 9

與Synchonized的對照:

ThreadLocal和Synchonized都用於解決多線程併發訪問。但是ThreadLocal與synchronized有本質的差異。synchronized是利用鎖的機制,使變量或代碼塊在某一時該僅僅能被一個線程訪問。而ThreadLocal爲每一個線程都提供了變量的副本,使得每一個線程在某一時間訪問到的並不是同一個對象,這樣就隔離了多個線程對數據的數據共享。而Synchronized卻正好相反,它用於在多個線程間通訊時能夠得到數據共享。

Synchronized用於線程間的數據共享,而ThreadLocal則用於線程間的數據隔離。

線程隔離特性

線程隔離特性,只有在線程內才能獲取到對應的值,線程外不能訪問。

(1)Synchronized是經過線程等待,犧牲時間來解決訪問衝突

(1)ThreadLocal是經過每一個線程單獨一份存儲空間,犧牲空間來解決衝突

須要瞭解ThreadLocal的源碼解析: 點此瞭解

4、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 使用完後必定要執行此方法。

5、什麼狀況下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就是前面說到的要惰性刪除的哈希槽。

6、ThreadLocal的最佳實踐

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

  • 每次使用完ThreadLocal實例,都調用它的remove()方法,清除Entry中的數據。

調用remove()方法最佳時機是線程運行結束以前的finally代碼塊中調用,這樣能徹底避免操做不當致使的內存泄漏,這種主動清理的方式比惰性刪除有效。

7、黃金分割 - 魔數0x61c88647

在這裏插入圖片描述

1.黃金分割數與斐波那契數列

首先複習一下斐波那契數列,下面的推導過程來自某搜索引擎的wiki:

斐波那契數列:1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, …
通項公式:假設F(n)爲該數列的第n項(n ∈ N*),那麼這句話能夠寫成以下形式:F(n) = F(n-1) + F(n-2)。
有趣的是,這樣一個徹底是天然數的數列,通項公式倒是用無理數來表達的。並且當n趨向於無窮大時,前一項與後一項的比值愈來愈逼近0.618(或者說後一項與前一項的比值小數部分愈來愈逼近0.618),而這個值0.618就被稱爲黃金分割數。證實過程以下:

在這裏插入圖片描述

黃金分割數的準確值爲(根號5 - 1)/2,約等於0.618。

2.黃金分割數的應用

黃金分割數被普遍使用在美術、攝影等藝術領域,由於它具備嚴格的比例性、藝術性、和諧性,蘊藏着豐富的美學價值,可以激發人的美感。固然,這些不是本文研究的方向,咱們先嚐試求出無符號整型和帶符號整型的黃金分割數的具體值:

public static void main(String[] args) throws Exception {
    //黃金分割數 * 2的32次方 = 2654435769 - 這個是無符號32位整數的黃金分割數對應的那個值
    long c = (long) ((1L << 32) * (Math.sqrt(5) - 1) / 2);
    System.out.println(c);
    //強制轉換爲帶符號爲的32位整型,值爲-1640531527
    int i = (int) c;
    System.out.println(i);
}

經過一個線段圖理解一下:
在這裏插入圖片描述
也就是2654435769爲32位無符號整數的黃金分割值,而-1640531527就是32位帶符號整數的黃金分割值。而ThreadLocal中的哈希魔數正是1640531527(十六進制爲0x61c88647)。爲何要使用0x61c88647做爲哈希魔數?這裏提早說一下ThreadLocal在ThreadLocalMap(ThreadLocal在ThreadLocalMap以Key的形式存在)中的哈希求Key下標的規則:

哈希算法:keyIndex = ((i + 1) * HASH_INCREMENT) & (length - 1)

其中,i爲ThreadLocal實例的個數,這裏的HASH_INCREMENT就是哈希魔數0x61c88647,length爲ThreadLocalMap中可容納的Entry(K-V結構)的個數(或者稱爲容量)。在ThreadLocal中的內部類ThreadLocalMap的初始化容量爲16,擴容後老是2的冪次方,所以咱們能夠寫個Demo模擬整個哈希的過程:

public class Main {

    private static final int HASH_INCREMENT = 0x61c88647;

    public static void main(String[] args) throws Exception {
        hashCode(4);
        hashCode(16);
        hashCode(32);
    }

    private static void hashCode(int capacity) throws Exception {
        int keyIndex;
        for (int i = 0; i < capacity; i++) {
            keyIndex = ((i + 1) * HASH_INCREMENT) & (capacity - 1);
            System.out.print(keyIndex);
            System.out.print(" ");
        }
        System.out.println();
    }
}

上面的例子中,咱們分別模擬了ThreadLocalMap容量爲4,16,32的狀況下,不觸發擴容,而且分別」放入」4,16,32個元素到容器中,輸出結果以下:

3 2 1 0 
7 14 5 12 3 10 1 8 15 6 13 4 11 2 9 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

每組的元素通過散列算法後剛好填充滿了整個容器,也就是實現了完美散列。實際上,這個並非偶然,其實整個哈希算法能夠轉換爲多項式證實:證實(x - y) HASH_INCREMENT != 2^n (n m),在x != y,n != m,HASH_INCREMENT爲奇數的狀況下恆成立,具體證實能夠自行完成。HASH_INCREMENT賦值爲0x61c88647的API文檔註釋以下:

連續生成的哈希碼之間的差別(增量值),將隱式順序線程本地id轉換爲幾乎最佳分佈的乘法哈希值,這些不一樣的
哈希值最終生成一個2的冪次方的哈希表。
相關文章
相關標籤/搜索