天天看點

ThreadLocal 原理 源碼分析 [MD]

博文位址

我的GitHub 我的部落格 我的微信 我的郵箱
baiqiantao bqt20094 [email protected]

目錄

  • ThreadLocal
    • 總結
      • 使用線程池的問題
      • 記憶體洩漏問題
  • 源碼分析
    • Thread
      • 構造方法
      • set() 方法
      • get() 方法
      • initialValue() 方法
    • ThreadLocalMap
      • 基礎結構
      • Hash 算法
      • Hash 沖突
    • InheritableThreadLocal
      • 實作原理
      • 測試代碼
    • 案例

ThreadLocal 為解決

多線程并發

問題提供了一種新的思路。使用它可以很簡潔地編寫出優美的多線程程式。當使用 ThreadLocal 維護變量時,ThreadLocal 為每個使用該變量的線程提供獨立的

變量副本

,是以每一個線程都可以獨立地改變自己的副本,而不會影響其它線程所對應的副本。

使用場景

  • 存儲單個線程上下文資訊
  • 使變量線程安全:變量既然成為了每個線程内部的局部變量,自然就不會存在并發問題了
  • 減少參數傳遞

ThreadLocal 提供了

線程獨有的局部變量(本地變量、副本變量)

,可以在整個線程存活的過程中随時取用,且線程之間互不幹擾。

  • ThreadLocal 裡 set 進去的資料,其實是存儲在目前 Thread 裡的(Thread 也是一個對象)
  • 每個 Thread 都有一個屬性

    threadLocals

    ,類型為

    ThreadLocal.ThreadLocalMap

    ,它本質上是一個自定義的

    map

  • 這個 map 的 entry 是

    ThreadLocal.ThreadLocalMap.Entry

    ,這個 entry 繼承自

    WeakReference<ThreadLocal<T>>

  • 這表明:

    ThreadLocal<?>

    被弱引用對象引用的對象

    ThreadLocal<T>

    (而非通過其 set 進去的資料)會在 gc 時被回收
  • 這個 map 其實隻是一個數組,map 的 get/set 方法的參數(也即象征意義的 key)是

    ThreadLocal<?>

  • 數組中存儲的值(也即象征意義的 value)即通過

    ThreadLocal<?>

    的 set 方法傳過來的 Object(類型就是裡面的泛型)
  • 由于 map 中對

    ThreadLocal<?>

    是通過弱引用的方式引用的,是以當

    ThreadLocal<?>

    不再被強引用時,此

    ThreadLocal<?>

    對象就會在 gc 時被回收

使用線程池可以達到線程複用的效果,但是歸還線程之前記得清除

ThreadLocalMap

,要不然再取出該線程的時候,

ThreadLocal

變量還會存在。

為何存在記憶體洩漏的問題

  • ThreadLocalMap

    中是以弱引用的方式引用的

    ThreadLocal

    ,如果一個

    ThreadLocal

    沒有外部強引用來引用它,那麼 GC 的時候,這個

    ThreadLocal

    就會被回收。
  • ThreadLocal

    被回收後,

    ThreadLocalMap

    中就會出現

    key

    null

    Entry

    ,此後,這些

    key

    null

    Entry

    value

    就沒有辦法通路了。
  • 由于這些

    key

    null

    Entry

    value

    存在一條強引用鍊:

    Thread -> ThreaLocalMap -> Entry -> value

    ,這就造成這些

    value

    永遠無法回收。
  • 如果目前線程遲遲不結束的話,就會造成記憶體洩漏。

如何解決記憶體洩漏的問題

在調用

ThreadLocal

get()、set()、remove()

的時候,都會主動清除掉

ThreadLocalMap

裡所有

key

null

value

需要明确,使用

ThreadLocal

肯定是存在記憶體洩漏的問題的,上面的方案雖然解決了部分場景的記憶體洩漏問題,但并不徹底!

首先從

Thread

類源碼入手:

public class Thread implements Runnable {
    ThreadLocal.ThreadLocalMap threadLocals = null; // 存儲普通的線程資料(本地變量)
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null; // 可以被子類繼承
}
           

Thread

類中有一個

threadLocals

和一個

inheritableThreadLocals

變量,它們都是

ThreadLocalMap

類型的變量。

預設情況下這兩個變量都是 null,隻有目前線程調用

ThreadLocal

類的

set

get

方法時才建立它們,實際上調用這兩個方法的時候,最終調用的是

ThreadLocalMap

類對應的方法。

ThreadLocalMap 是 ThreadLocal 的内部類,本質是一個自定義的 map。

public ThreadLocal()
protected T initialValue()
public void set(T value)
public T get()
public void remove()
           

private final int threadLocalHashCode = nextHashCode();  //用來在 map 中找到自己

public ThreadLocal() {}
           

ThreadLocal 執行個體的變量隻有一個

threadLocalHashCode

,用來在 ThreadLocalMap 中找到自己存儲的位置

public void set(T value) {
    Thread t = Thread.currentThread(); // 擷取目前線程
    ThreadLocalMap map = t.threadLocals; // 傳回目前線程中的成員變量 threadLocals
    if (map != null) map.set(this, value); // 注意是将該 ThreadLocal 執行個體作為key
    else t.threadLocals = new ThreadLocalMap(this, value); // 初始化線程中的成員變量,并指派
}
           

set

方法很簡單,主要是判斷

ThreadLocalMap

是否存在,然後使用

ThreadLocalMap

中的

set

方法進行資料處理。具體邏輯在後面剖析

ThreadLocalMap

源碼時再看。

通過上面的

set

方法及下面

get

方法可知,通過

ThreadLoca

set

方法存儲的對象,都是存儲在目前線程對象的

ThreadLocalMap

中的,其他線程通路不到,各個線程中通過

get

方法通路的是不同的對象。

public T get() {
    Thread t = Thread.currentThread(); // 擷取目前線程
    ThreadLocalMap map = t.threadLocals; // 擷取目前線程中的 ThreadLocalMap
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this); // 擷取 ThreadLocal 對應的鍵值對
        if (e != null) return (T)e.value; // 如果鍵值對存在,則傳回目前 ThreadLocal 對應的值
    }
    return setInitialValue(); // 如果鍵值對不存在,則傳回 initialValue() 方法指定的初始值
}
           

If the variable has no value for the current thread, it is first initialized to the value returned by an invocation of the

initialValue()

method.

private T setInitialValue() {
    T value = initialValue(); // 擷取指定的初始值
    set(value); // 中間這一塊的邏輯和 set 方法完全一樣
    return value;
}
           

get

方法的邏輯也很簡單,如果通過目前

ThreadLocal

能在

map

中找到非空的

Entry

,則正常傳回

Entry

中的值(也即之前通過

set

方法存儲的值),否則傳回

initialValue()

方法指定的初始值。

核心邏輯依舊在

ThreadLocalMap

getEntry(tl)

方法中,具體邏輯同樣在後面剖析

ThreadLocalMap

initialValue()

方法僅在

get

方法中被調用,用于傳回

ThreadLocal

對應的初始化值,一般作為匿名内部類使用。

關于

initialValue()

的注意事項:

  • Returns the current thread's "initial value" for this thread-local variable.
  • This method will be invoked the first time a thread accesses the variable with the

    get

    method, unless the thread previously invoked the

    set

    method, in which case the initialValue() method will not be invoked for the thread.
  • 注意這個方法可能不調用,也可能調用多次,不能在裡面做初始化邏輯
一般沒啥用的一個方法

ThreadLocalMap

類似

HashMap

的結構,隻是

HashMap

是由數組+連結清單實作的,而

ThreadLocalMap

中并沒有連結清單結構。

ThreadLocalMap is a

customized hash map

suitable 适用于 only for maintaining 維護 thread local values.

static class ThreadLocalMap {
    //The table, resized as necessary. table.length MUST always be a power of two.
    private Entry[] table; // 可自動擴容的數組

    static class Entry extends WeakReference<ThreadLocal<?>> {
        Object value; // The value associated with this ThreadLocal
        Entry(ThreadLocal<?> k, Object v) {
            super(k); // key
            value = v; // value
        }
    }
    //...
}
           
  • ThreadLocalMaps are constructed

    lazily

    , so we only create one when we have at least one entry to

    put

    in it.
  • To help deal with very large and long-lived 長期存在 usages, the hash table entries use

    WeakReferences

    for

    keys

    .
  • However, since

    ReferenceQueue

    are not used, stale 過時的 entries are guaranteed 確定 to be removed only when the table starts running out of space 沒有空間.
  • Note that

    null keys

    mean that the key is no longer referenced, so the entry can be expunged 移除 from table.

int i = key.threadLocalHashCode & (table.len-1); // 計算目前 key 在散清單中對應的數組下标位置
           

這裡最關鍵的就是

threadLocalHashCode

值的計算,這個值其實在建立 ThreadLocal 時已經計算好了。

private final int threadLocalHashCode = nextHashCode(); // ThreadLocal 的 hash 值

private static int nextHashCode() {
    return nextHashCode.getAndAdd(HASH_INCREMENT); // Atomically adds the given value to the current value.
}

private static AtomicInteger nextHashCode = new AtomicInteger(); //The next hash code to be given out. Updated atomically. Starts at zero.
private static final int HASH_INCREMENT = 0x61c88647;
           

每當建立一個

ThreadLocal

對象,這個

ThreadLocal.nextHashCode

這個值就會在之前的基礎上增長

0x61c88647

。這個值很特殊,它是斐波那契數也叫黃金分割數,hash 增量為這個數字的好處就是:hash 分布非常均勻。

HashMap

中解決沖突的方法是

連結清單法

,沖突的資料挂載到連結清單上,如果連結清單長度超過一定數量則會轉化成紅黑樹。

ThreadLocalMap

開放位址法

(

線性探測法

):插入一個

Entry

時,如果通過

hash

計算的槽位中已經有了

Entry

資料,此時就會線性向後查找,一直找到

Entry

null

的槽位才會停止查找,并将目前元素放入此槽位中

ThreadLocalMap

set

資料分為以下幾種情況:

  • 通過

    hash

    計算後的槽位對應的

    Entry

    資料為空:直接将資料放到該槽位即可
  • hash

    Entry

    資料不為空
    • 如果

      key

      值與目前

      ThreadLocal

      hash

      計算擷取的

      key

      值一緻:更新該槽位的資料即可
    • key

      ThreadLocal

      hash

      key

      值不一緻:線性向後查找
      • 往後周遊過程中,在找到

        Entry

        null

        的槽位之前,沒有遇到

        key

        過期的

        Entry

        :周遊散列數組,線性往後查找,如果找到

        Entry

        null

        的槽位,則将資料放入該槽位中,或者往後周遊過程中,遇到了

        key

        值相等的資料,直接更新即可
      • Entry

        null

        的槽位之前,遇到了

        key

        Entry

        :會進行一輪探測式清理操作,具體邏輯就不去理了,意義不大

set()

方法其實做了很多事情,包括:添加資料、更新資料、清理資料、優化資料桶的位置、數組擴容,具體邏輯就不去理了,意義不大。

get

的邏輯和

set

類似,分為以下幾種情況:

  • hash

    Entry.key

    和查找的

    key

    一緻:則直接傳回
  • hash

    Entry.key

    key

    不一緻:則往後疊代查找,查找過程中也會進行一輪探測式清理操作

使用

ThreadLocal

的時候,在異步場景下是無法給

子線程

共享

父線程

中建立的線程副本資料的。使用

InheritableThreadLocal

便可以解決這個問題。

實作原理很簡單:在父線程中通過

new Thread()

建立子線程時,

Thread#init

方法會在

Thread

的構造方法中被調用,在

init

方法中會拷貝父線程的

InheritableThreadLocal

中的資料到子線程中:

private void init(...boolean inheritThreadLocals) {
    if (inheritThreadLocals && parent.inheritableThreadLocals != null)
        this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
}
           
static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
	return new ThreadLocalMap(parentMap);
}
           

ThreadLocal<String> threadLocal = new ThreadLocal<>();
ThreadLocal<String> local = new InheritableThreadLocal<>();
threadLocal.set("白乾濤");
local.set("白乾濤");

ThreadLocal<String> threadLocal2 = ThreadLocal.withInitial(() -> "白乾濤2");
ThreadLocal<String> local2 = InheritableThreadLocal.withInitial(() -> "白乾濤2");

new Thread(() -> {
	System.out.println("子線程擷取父線程ThreadLocal資料:" + threadLocal.get() + "-" + threadLocal2.get()); // null-白乾濤2
	System.out.println("子線程擷取父線程InheritableThreadLocal資料:" + local.get() + "-" + local2.get()); // 白乾濤-白乾濤2
}).start();
           

public class Test {
    ThreadLocal<String> mNameLocal = new ThreadLocal<>();
    ThreadLocal<Long> mIdLocal = ThreadLocal.withInitial(() -> {
        System.out.println(Thread.currentThread().getName() + " 調用了 get");
        return -1L; //初始化資料
    });

    public static void main(String[] args) throws InterruptedException {
        new Test().test();
    }

    private void test() throws InterruptedException {
        mNameLocal.set(Thread.currentThread().getName()); //更新【主線程】資料
        mIdLocal.set(Thread.currentThread().getId()); //更新【主線程】資料

        Thread thread = new Thread(() -> {
            mNameLocal.set(Thread.currentThread().getName()); //更新【子線程】資料
            //mIdLocal.set(Thread.currentThread().getId()); //更新【子線程】資料
            System.out.println(mNameLocal.get() + " " + mIdLocal.get()); //Thread-0 -1
        });
        thread.start();
        thread.join(); //效果等同于同步:等 thread 執行完畢後再執行下面的邏輯

        System.out.println(mNameLocal.get() + " " + mIdLocal.get()); //main 1
        mNameLocal.remove();
        mIdLocal.remove();
        System.out.println(mNameLocal.get() + " " + mIdLocal.get()); //null -1
    }
}
           

列印結果:

Thread-0 調用了 get
Thread-0 -1
main 1
main 調用了 get
null -1
           

2019-1-21

本文來自部落格園,作者:白乾濤,轉載請注明原文連結:https://www.cnblogs.com/baiqiantao/p/7257326.html

繼續閱讀