1. 程式人生 > >ThreadLocal 實現原理總結

ThreadLocal 實現原理總結

ThreadLocal 用於在不同執行緒中互不干擾的儲存並提供資料。

這裡不對原始碼進行深究,只淺顯的對實現原理進行了解。

本次涉及到的原始碼為 Source for Android 27.


ThreadLocal 的實現,需要藉助到 ThreadLocalMap

需要提前交代的:
在一個 Thread 例項內部,都有一個 threadLocals 成員變數(ThreadLocalMap 型別),而這個 threadLocals 內部又維護了一個 Entry 型別的陣列。

Entry 是一個 key - value 實體,用於儲存 ThreadLocal - Object

鍵值實體。


先看一段程式碼:

@Override
protected void onCreate(Bundle savedInstanceState) {
    setContentView(R.layout.activity_main3);
    
    ThreadLocal<String> threadLocal = new ThreadLocal<>();
    
    new Thread(() -> {
        threadLocal.set("Thread 1");
        Log.d("Test", "Thread 1 => "+threadLocal.get());
    }).start();
    
    threadLocal.set("Main");
    Log.d("Test", "MainThread => "+threadLocal.get());

}

在這段程式碼裡面,生成了一個 ThreadLocal<String> 例項 threadLocal,然後分別在 UI 執行緒和一個子執行緒裡面去分別進行 set()get() 操作。然後,就會在不同執行緒裡面列印對應的值。


首先看 threadLocal.set() 方法內部:

//	ThreadLocal.set()
public void set(T value) {
    Thread t = Thread.currentThread();//獲得呼叫 set() 方法所處的執行緒例項,即 Thread 例項
    ThreadLocalMap map = getMap(t);//進一步獲得 Thread 例項的 threadLocals(ThreadLocalMap 型別) 成員變數
    if (map != null)
        map.set(ThreadLocal.this, value);
    else
        //如果 t 的 threadLocals == null,則新建一個
        //新建的時候會將當前 ThreadLocal 的 this 引用傳遞進去
        createMap(t, value);
}

//	ThreadLocal.getMap()
ThreadLocalMap getMap(Thread t) {
    //每個 Thread 例項都會有一個 threadLocals 成員變數
    return t.threadLocals;
}

在某一執行緒裡面呼叫 threadLocalset(value) 方法,那麼 Thread.currentThread() 就會得到當前執行緒的例項,然後通過該 Thread 例項獲取到其內部的成員變數 threadLocalsThreadLocalMap 型別),然後將當前 threadLocal 例項作為 key 值,與形參 value 繫結在一起生成一個 Entry 例項(當然這裡是通常情況,不考慮重複的 key 而替換原值的情況),並存儲到 ThreadLocalMap 內部的 Entry[] 中。

//	ThreadLocalMap.set()
private void set(ThreadLocal<?> key, Object value) {
    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)]) {
        ThreadLocal<?> k = e.get();
        // 如果是重複的 key 值,則替換原有的 value 值
        // 這就對應著在同一個執行緒呼叫兩次 threadLocal.set() 情況
        if (k == key) {
            e.value = value;
            return;
        }
        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }
    tab[i] = new Entry(key, value);
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

通過上述內容,可以得到如下關係:
在這裡插入圖片描述

通過 threadLocal.set(value) 就將 threadLocal 例項與 value 繫結在一起存放在了當前執行緒(即 Thread 例項)之中(這是從模糊的概念上來說),或者可以說,當前執行緒根據 threadLocal 例項作為索引,可以儲存對應的 value 值。

只要理解了 threadLocal.set(value) 大致的原理,那麼對於 threadLocal.get() 方法也容易理解了。

//	ThreadLocal
public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}

threadLocal.get() 的時候,就會根據當前執行緒( Thread 例項)得到對應的 threadLocalsThreadLocalMap 型別),再進一步得到 Entry[]key 值為 threadLocalEntry 例項,最後獲得 Entry 例項的 value 值。


上述,就是 ThreadLocalset(value)get() 的大致實現,雖然有點繞,但是仔細體會一下,還是容易理解的。

彩蛋部分:
另外,我在自己琢磨到似懂非懂的狀態時,就突然產生了一個疑問,為什麼 ThreadLocalMap 內部要維護一個 Entry[] (陣列),而不是單個的 Entry 例項,因為明明只要儲存 threadLocal-value 的鍵值對。

後來,突然一下就想明白了,看了下面的程式碼,也許就能體會到了。

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main3);
    
    ThreadLocal<Boolean> threadLocal1 = new ThreadLocal<>();
    ThreadLocal<String> threadLocal2 = new ThreadLocal<>();
    
    new Thread(() -> {
        threadLocal1.set(true);
        threadLocal2.set("Thread 1");
        Log.d("Test", "Thread 1 => "+threadLocal1.get());
        Log.d("Test", "Thread 1 => "+threadLocal2.get());
    }).start();
    
    threadLocal1.set(false);
    threadLocal2.set("Main");
    Log.d("Test", "MainThread => "+threadLocal1.get());
    Log.d("Test", "MainThread => "+threadLocal2.get());
}

對於每一個執行緒(即 Thread 例項)來說,可能遇到需要維護多個不同的 threadLocal-value 的情況(即會產生多個不同的 Entry 例項),因此,就需要一個 Entry[] 來儲存多個 Entry 例項。


最後,還需要說明的一點是,ThreadLocal 的作用是為不同的執行緒儲存對應的物件(實際上是物件的引用,想想 String 型別),如果在兩個執行緒中儲存的都是同一個物件的引用,那麼在兩個執行緒中得到也必然會是同一物件的引用,這點是需要注意的。

public class Test {
    int anInt;
    public static void main(String[] args) {
        Test t = new Test();
        t.anInt = 3;
        ThreadLocal<Test> local = new ThreadLocal<>();
        local.set(t);
        local.get().t(10);
        new Thread(() -> {
            local.set(t);
            System.out.println(local.get().anInt);
        }).start();
        System.out.println(local.get().anInt);
    }
    public int t(int v)  {
        anInt = v;
        return anInt;
    }
}

列印結果:
10
10


可供參考的文章:

ThreadLocal四重奏 系列