Java 併發程式設計:ThreadLocal 的使用及其原始碼實現

WngShhng發表於2018-07-10

1、ThreadLocal的使用

防止任務在共享資源上產生衝突的一種方式是根除對變數的共享,使用執行緒的本地儲存為使用相同變數的不同執行緒建立不同的儲存。

下面是一個 ThreadLocal 的例項。這裡我們使用了靜態的全域性變數 ThreadLocal 物件來儲存 Integer 型別的值。我們在不同的執行緒中將指定的數字傳入到 threadLocal 中進行儲存。然後,再將其讀取出來:

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

    public static void main(String...args) {
        threadLocal.set(-1);
        ExecutorService executor = Executors.newCachedThreadPool();
        for (int i=0; i<5; i++) {
            final int ii = i; // i不能是final的,建立臨時變數
            executor.submit(new Runnable() {
                public void run() {
                    threadLocal.set(ii);
                    System.out.println(threadLocal.get());
                }
            });
        }
        executor.shutdown();
        System.out.println(threadLocal.get());
    }
複製程式碼

從程式的執行結果可以看出,每個執行緒都正確地讀取出來了儲存到 ThreadLocal 中的資料。

所以,我們總結一下 ThreadLocal 的作用就是,儲存在 ThreadLocal 中的變數是執行緒安全的,每個執行緒只能讀取出自己儲存的值。

通常它的使用方式就是定義一個靜態全域性的 ThreadLocal 例項,然後每個執行緒使用它來讀寫只有自己會用到的資料。比如,我們要為每個執行緒建立了一個資料庫連線,並且該連線只允許該執行緒自己使用,那麼可以將它儲存在 ThreadLocal 中,然後在用到的地方獲取。

看了上面的例子,也許你會又以下一些問題:

  1. ThreadLocal中儲存的值是如何保證絕對的執行緒安全的?
  2. 那麼這些值是儲存在什麼地方?
  3. 是靜態型別的還是例項型別的?
  4. 如果某個執行緒執行完畢了,被銷燬了,那麼這些儲存的值會被怎麼處理呢?
  5. ……

帶著上面的這些問題,我們來看下在JDK原始碼中 ThreadLocal 是如何實現的。

2、ThreadLocal的作用原理

我們還是先從讀取的操作來看。

以下是 ThreadLocalset() 方法的程式碼:

    public T get() {
        Thread t = Thread.currentThread();  // 1
        ThreadLocalMap map = getMap(t);  // 2
        if (map != null) {  // 3
            ThreadLocalMap.Entry e = map.getEntry(this);  // 4
            if (e != null) {
                T result = (T) e.value; // 5
                return result;
            }
        }
        return setInitialValue();
    }
複製程式碼

這裡首先會再步驟1中獲取到當前執行緒的例項,然後在步驟2中通過getMap()方法,使用當前的執行緒的ThreadLocalMap。這裡的ThreadLocalMap的定義如下:

    static class ThreadLocalMap {

        static class Entry extends WeakReference<ThreadLocal<?>> {
            Object value;
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
		
        private Entry[] table;
		
        private Entry getEntry(ThreadLocal<?> key) {
            int i = key.threadLocalHashCode & (table.length - 1);
            Entry e = table[i];
            if (e != null && e.get() == key)
                return e;
            else
                return getEntryAfterMiss(key, i, e);
        }
		
        // ...
    }
複製程式碼

然後,我們看下getMap()方法的定義:

    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
複製程式碼

也就是說實際上當我們呼叫 get() 方法的時候,會先獲取當前執行緒中的 threadLocals 欄位,該欄位是 ThreadLocalMap 型別的。然後,我們使用當前的 ThreadLocal 例項作為鍵來從雜湊表中獲取到一個 Entry,而實際的值就儲存再 Entryvalue 欄位中。

就像上面的 getEntry() 方法定義的那樣,似乎這裡的雜湊表只是一個陣列,那雜湊衝突是怎麼解決的呢?實際上,我們知道通常解決雜湊衝突有兩種解決方式,一種是拉鍊法,一種是線性探測法。前者在 HashMapConcurrentHashMap 中使用較多,而這裡用到的其實就是線性探測法。說白了就是將所有的值放在一個陣列裡面然後根據雜湊的結果到陣列中取值,具體的實現方式可以看相關的資料結構知識點。

這裡的關係是不是有點亂,我們來捋一下:

我們使用ThreadLocal儲存的值實際是儲存在Thread使用ThreadLocalMap當中的,而這裡的ThreadLocal例項值起到了一個雜湊表的鍵的作用:

ThreadLocal

就像上圖顯示的那樣,假如我們線上程thread1中呼叫了threadLocal1get()方法,首先會用Thread.currentThread()方法獲取到thread1,然後獲取到thread1threadLocals例項,threadLocals是一個ThreadLocalMap型別的雜湊表。然後,我們再用threadLocal1作為鍵來從threadLocals中獲取到值Entry,並從Entry中取出儲存的值並返回。

至此,我們已經瞭解了ThreadLocal的實現的原理,本來想看下set()方法的,但是到此已經基本真相大白了,所以也就沒有繼續下去的必要了。

3、總結

我們回過頭來看下之前提出的幾個問題:

  1. ThreadLocal中儲存的值是如何保證絕對的執行緒安全的? 實際上每個值都是存線上程內部的,ThreadLocal只用來幫助我們從該執行緒內部的雜湊表中找到存放的那個值。
  2. 那麼這些值是儲存在什麼地方?執行緒內部的例項欄位。
  3. 是靜態型別的還是例項型別的?執行緒內部的例項欄位。
  4. 如果某個執行緒執行完畢了,被銷燬了,那麼這些儲存的值會被怎麼處理呢?因為是執行緒的區域性欄位,所以執行緒不在了,值就沒有了。

以上就是ThreadLocal的用法和實現原理。

相關文章