從實例和源碼角度簡析 ThreadLocal

注:此文源碼摘自 sun jdk 1.8

ThreadLocal 是什麼

打開 ThreadLocal 的源碼我們可以看到如下的註釋:

這裏寫圖片描述

大致翻譯如下:

該類提供了線程局部 (thread-local) 變量。這些變量不同於它們的普通對應物,因爲訪問某個變量(通過其 get 或 set 方法)的每個線程都有自己的局部變量,它獨立於變量的初始化副本。ThreadLocal 實例通常是類中的 private static 字段,它們希望將狀態與某一個線程(例如,用戶 ID 或事務 ID)相關聯。

例如,以下類生成對每個線程唯一的局部標識符。 線程 ID 是在第一次調用 UniqueThreadIdGenerator.getCurrentThreadId() 時分配的,在後續調用中不會更改。

  import java.util.concurrent.atomic.AtomicInteger;

 public class UniqueThreadIdGenerator {

     private static final AtomicInteger uniqueId = new AtomicInteger(0);

     private static final ThreadLocal < Integer > uniqueNum = 
         new ThreadLocal < Integer > () {
             @Override protected Integer initialValue() {
                 return uniqueId.getAndIncrement();
         }
     };

     public static int getCurrentThreadId() {
         return uniqueId.get();
     }
 } // UniqueThreadIdGenerator

每個線程都保持對其線程局部變量副本的隱式引用,只要線程是活動的並且 ThreadLocal 實例是可訪問的;在線程消失之後,其線程局部實例的所有副本都會被垃圾回收(除非存在對這些副本的其他引用)。

簡單而言,ThreadLocal 是一個線程自身內部的數據存儲類,其它線程是無法訪問該線程的數據的。這樣說起來還是挺抽象的,我們下面來介紹一個例子。

ThreadLocal 的使用

代碼如下:

public class Test {
    private static final ThreadLocal<String> threadLocal = new ThreadLocal<String>() {
        @Override
        protected String initialValue() {
            return "default";
        }
    };

    public static void main(String[] args) {
        threadLocal.set("thread#0");
        System.out.println("thread#0 " + threadLocal.get());

        new Thread("thread#1") {
            @Override
            public void run() {
                System.out.println(currentThread().getName() + " " + threadLocal.get());
            }
        }.start();

        new Thread("thread#2") {
            @Override
            public void run() {
                threadLocal.set("thread#2");
                System.out.println(currentThread().getName() + " " + threadLocal.get());
            }
        }.start();
    }
}

我們首先創建一個 ThreadLocal<String> 對象,並重寫它的 initialValue() 方法,這樣它默認情況下就是返回一個字符串爲 default 的字段。然後我們再 main() 方法裏面創建兩個線程,線程 thread#1thread#2,當然,加上它本身,一共是三個線程,我們分別在主線程調用 set("thread#0")thread#1 線程不調用 set() 方法;thread#2 調用 set("thread#2") 方法。我們發現,輸出結果如下:

這裏寫圖片描述

對於主線程,由於我們調用了 set("thread#0") 方法,所以 threadLocal.get() 的值就是字符串 thread#0;對於 thread#1 線程沒有調用 set() 方法,故 threadLocal.get() 返回的就是默認值 default 字符串;對於 thread#2 線程,我們調用了 set("thread#2") 方法,故 threadLoca.get() 返回的就是字符串 thread#2

所以我們可以發現,即使是同一個 ThreadLocal 對象,我們的 set()get() 方法都是僅對當前線程可見的,各個線程之間不可見不可相互影響。

ThreadLocal 源碼解析

要想搞清楚 ThreadLocal 爲什麼會有這樣的特性,我們其實只需要搞清楚我們上面所使用到的 set()get() 方法即可。set() 方法源碼如下:

這裏寫圖片描述

第一行代碼不解釋了。我們看到第二行代碼可以獲取到一個 ThreadLocalMap,我們不妨戳進 getMap() 方法裏面看一下,ThreadLocalMap 是如何和 Thread 參數關聯在一起的,源碼如下:

這裏寫圖片描述

我們再戳進 Thread 類看一下 ——

這裏寫圖片描述

這下清楚了,先獲取當前的線程對象,再獲取這個對象中的 ThreadLocalMap 對象,並且調用它的 set() 方法將當前的 ThreadLocal 對象和傳入的 value 值存入。那麼 ThreadLocalMap 又是個什麼呢?它的 set() 方法肯定是個關鍵點,又是怎樣的呢?我們再來看看 ——

這裏寫圖片描述

ThreadLocalMap 其實就是 ThreadLocal 的一個內部類

這裏寫圖片描述

這裏寫圖片描述

到這裏就一目瞭然了,ThreadLocalMap 中維護了一個 Entry[]Entry 是一個泛型類,其構造函數需要傳入兩個參數,一個是 ThreadLocal 對象,作爲 Reference,另一個是 Object 對象,作爲 value),然後我們就只需要將當前的 ThreadLocal 對象和傳入的值放入這個數組的某個位置就可以了。

接下來我們再來看看 get() 方法,其源碼如下:

這裏寫圖片描述

這個源碼很簡單了,就是獲取到當前線程對象的所持有的 ThreadLocalMap 對象,傳入當前的 ThreadLocal 對象由此獲得到對應的 ThreadLocalMap.Entry 對象,再取出它的 value 即可。

到此所有的流程已經走了一遍了,我們再來理一遍思路:首先創建一個全局共享的 ThreadLocal 對象,因爲 ThreadLocal 不應該是依賴某一個具體的線程的,然後假設我們使用了三個線程 A、B、C 來對該 ThreadLocal 對象進行操作,線程 A 先調用 ThreadLocal 對象的 set() 方法,那麼線程 A 中的 ThreadLocalMap 裏面的 Entry 數組就會在某個位置保存這個 ThreadLocal 對象和 set() 進來的值;然後線程 B 調用 ThreadLocal 對象的 set() 方法,那麼線程 B 中的 ThreadLocalMap 裏面的 Entry 數組就會在某個位置保存這個 ThreadLocal 對象和 set() 進來的值;然後線程 C 調用 ThreadLocal 對象的 set() 方法,那麼線程 C 中的 ThreadLocalMap 裏面的 Entry 數組就會在某個位置保存這個 ThreadLocal 對象和 set() 進來的值。每個線程所持有的 ThreadLocalMap 對象是不同的,故其 ThreadLocalMap 裏面的 Entry 數組也是不同的,故它們之間的 set()get() 方法是互不相見的。

ThreadLocal 使用場景

在 Android 中,ThreadLocal 比較知名的一個場景就是 Looper 的使用,我們可以在 Lopper 類的源碼中找到 ThreadLocal 的影子,因爲一個線程對應一個 Looper,這時使用 ThreadLocal 就再好不過了。那麼爲什麼要在 Looper 中使用 ThreadLocal,而不能是別的什麼替代方案呢?假如我們不使用 ThreadLocal,那麼我們可以使用一個全局的哈希表來維護線程和 Looper 的關係,這樣顯然不如 ThreadLocal 來的方便。一般來說,當某些數據是以線程爲作用域並且不同線程之間有不同的數據副本的話,就可以採用 ThreadLocal

擴展鏈接:理解Java中的ThreadLocal

發佈了41 篇原創文章 · 獲贊 81 · 訪問量 15萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章