面試中的 ThreadLocal 原理和使用場景

Java極客技術發表於2019-07-27

相信大家不管是在網上做題還是在面試中都經常被問過 ThreadLocal 的原理和用法,雖然一直知道這個東西的存在但是一直沒有好好的研究一下原理,沒有自己的知識體系。今天花點時間好好學習了一下,分享給有需要的朋友。

ThreadLocal 是什麼

ThreadLocal 是 JDK java.lang 包中的一個用來實現相同執行緒資料共享不同的執行緒資料隔離的一個工具。 我們來看下 JDK 原始碼中是如何解釋的:

This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its get or set method) has its own, independently initialized copy of the variable. 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).

Each thread holds an implicit reference to its copy of a thread-local variable as long as the thread is alive and the ThreadLocal instance is accessible; after a thread goes away, all of its copies of thread-local instances are subject to garbage collection (unless other references to these copies exist).

大致的意思是

ThreadLocal 這個類提供執行緒區域性變數,這些變數與其他正常的變數的不同之處在於,每一個訪問該變數的執行緒在其內部都有一個獨立的初始化的變數副本;ThreadLocal 例項變數通常採用private static在類中修飾。

只要 ThreadLocal 的變數能被訪問,並且執行緒存活,那每個執行緒都會持有 ThreadLocal 變數的副本。當一個執行緒結束時,它所持有的所有 ThreadLocal 相對的例項副本都可被回收。

一句話說就是 ThreadLocal 適用於每個執行緒需要自己獨立的例項且該例項需要在多個方法中被使用(相同執行緒資料共享),也就是變數線上程間隔離(不同的執行緒資料隔離)而在方法或類間共享的場景。

ThreadLocal 使用

我們先通過兩個例子來看一下 ThreadLocal 的使用

例子 1 普通變數

import java.util.concurrent.CountDownLatch;public class MyStringDemo {    private String string;    private String getString() {        return string;    }    private void setString(String string) {        this.string = string;    }    public static void main(String[] args) {        int threads = 9;        MyStringDemo demo = new MyStringDemo();        CountDownLatch countDownLatch = new CountDownLatch(threads);        for (int i = 0; i < threads; i++) {            Thread thread = new Thread(() -> {                demo.setString(Thread.currentThread().getName());                System.out.println(demo.getString());                countDownLatch.countDown();            }, "thread - " + i);            thread.start();        }    }}

程式的執行的隨機結果如下:

thread - 1thread - 2thread - 1thread - 3thread - 4thread - 5thread - 6thread - 7thread - 8Process finished with exit code 0

從結果我們可以看出多個執行緒在訪問同一個變數的時候出現的異常,執行緒間的資料沒有隔離。下面我們來看下采用 ThreadLocal 變數的方式來解決這個問題的例子。

例子 2 ThreadLocal 變數

import java.util.concurrent.CountDownLatch;public class MyThreadLocalStringDemo {    private static ThreadLocal<String> threadLocal = new ThreadLocal<>();    private String getString() {        return threadLocal.get();    }    private void setString(String string) {        threadLocal.set(string);    }    public static void main(String[] args) {        int threads = 9;        MyThreadLocalStringDemo demo = new MyThreadLocalStringDemo();        CountDownLatch countDownLatch = new CountDownLatch(threads);        for (int i = 0; i < threads; i++) {            Thread thread = new Thread(() -> {                demo.setString(Thread.currentThread().getName());                System.out.println(demo.getString());                countDownLatch.countDown();            }, "thread - " + i);            thread.start();        }    }}

程式執行結果

thread - 0thread - 1thread - 2thread - 3thread - 4thread - 5thread - 6thread - 7thread - 8Process finished with exit code 0

從結果來看,這次我們很好的解決了多執行緒之間資料隔離的問題,十分方便。

這裡可能有的朋友會覺得在例子 1 中我們完全可以通過加鎖來實現這個功能。是的沒錯,加鎖確實可以解決這個問題,但是在這裡我們強調的是執行緒資料隔離的問題,並不是多執行緒共享資料的問題假如我們這裡除了getString() 之外還有很多其他方法也要用到這個 String,這個時候各個方法之間就沒有顯式的資料傳遞過程了,都可以直接中 ThreadLocal 變數中獲取,這才是 ThreadLocal 的核心,相同執行緒資料共享不同的執行緒資料隔離

由於ThreadLocal 是支援泛型的,這裡採用的是存放一個 String 來演示,其實可以存放任何型別,效果都是一樣的。

ThreadLocal 原始碼分析

在分析原始碼前我們明白一個事那就是物件例項與 ThreadLocal 變數的對映關係是由執行緒 Thread 來維護的物件例項與 ThreadLocal 變數的對映關係是由執行緒 Thread 來維護的物件例項與 ThreadLocal 變數的對映關係是由執行緒 Thread 來維護的重要的事情說三遍。換句話說就是物件例項與 ThreadLocal 變數的對映關係是存放的一個 Map 裡面(這個 Map 是個抽象的 Map 並不是 java.util 中的 Map ),而這個 Map 是 Thread 類的一個欄位!而真正存放對映關係的 Map 就是 ThreadLocalMap下面我們通過原始碼的中幾個方法來看一下具體的實現。

//set 方法public void set(T value) {    Thread t = Thread.currentThread();    ThreadLocalMap map = getMap(t);    if (map != null)        map.set(this, value);    else        createMap(t, value);}//獲取執行緒中的ThreadLocalMap 欄位!!ThreadLocalMap getMap(Thread t) {    return t.threadLocals;}//建立執行緒的變數void createMap(Thread t, T firstValue) {     t.threadLocals = new ThreadLocalMap(this, firstValue);}

 set 方法中首先獲取當前執行緒,然後通過 getMap 獲取到當前執行緒的 ThreadLocalMap 型別的變數 threadLocals如果存在則直接賦值,如果不存在則給該執行緒建立 ThreadLocalMap 變數並賦值。賦值的時候這裡的 this 就是呼叫變數的物件例項本身。

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();}private T setInitialValue() {    T value = initialValue();    Thread t = Thread.currentThread();    ThreadLocalMap map = getMap(t);    if (map != null)        map.set(this, value);    else        createMap(t, value);    return value;}

get 方法也比較簡單,同樣也是先獲取當前執行緒的 ThreadLocalMap 變數,如果存在則返回值,不存在則建立並返回初始值。

ThreadLocalMap 原始碼分析

ThreadLocal 的底層實現都是通過 ThreadLocalMap 來實現的,我們先看下 ThreadLocalMap 的定義,然後再看下相應的 set  get 方法

static class ThreadLocalMap {    /**     * The entries in this hash map extend WeakReference, using     * its main ref field as the key (which is always a     * ThreadLocal object).  Note that null keys (i.e. entry.get()     * == null) mean that the key is no longer referenced, so the     * entry can be expunged from table.  Such entries are referred to     * as "stale entries" in the code that follows.     */    static class Entry extends WeakReference<ThreadLocal<?>> {        /** The value associated with this ThreadLocal. */        Object value;        Entry(ThreadLocal<?> k, Object v) {            super(k);            value = v;        }    }    /**     * The table, resized as necessary.     * table.length MUST always be a power of two.     */    private Entry[] table;}

ThreadLocalMap 中使用 Entry[] 陣列來存放物件例項與變數的關係,並且例項物件作為 key,變數作為 value 實現對應關係。並且這裡的 key 採用的是對例項物件的弱引用,(因為我們這裡的 key 是物件例項,每個物件例項有自己的生命週期,這裡採用弱引用就可以在不影響物件例項生命週期的情況下對其引用)。

private void set(ThreadLocal<?> key, Object value) {    Entry[] tab = table;    int len = tab.length;    //獲取 hash 值,用於陣列中的下標    int i = key.threadLocalHashCode & (len-1);    //如果陣列該位置有物件則進入    for (Entry e = tab[i];         e != null;         e = tab[i = nextIndex(i, len)]) {        ThreadLocal<?> k = e.get();        //k 相等則覆蓋舊值        if (k == key) {            e.value = value;            return;        }        //此時說明此處 Entry 的 k 中的物件例項已經被回收了,需要替換掉這個位置的 key 和 value        if (k == null) {            replaceStaleEntry(key, value, i);            return;        }    }    //建立 Entry 物件    tab[i] = new Entry(key, value);    int sz = ++size;    if (!cleanSomeSlots(i, sz) && sz >= threshold)        rehash();}//獲取 Entryprivate 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);}

至此我們看完了 ThreadLocal 相關的 JDK 原始碼,我自己也有了更深入的瞭解,也希望能幫助到大家。

小結

在平時忙碌的工作中我們經常解決的是一個業務的需求,往往很少會涉及到底層的原始碼或者框架的具體實現程式碼。 其實這是很不好的,其實很多的東西的原理都是一樣的,我們需要經常去看一下原始碼,瞭解一些底層的實現,不能總是停留在表層,程式碼看到多了,才能寫出好的程式碼,並且還能學到很多東西。 隨著我們知道的越來越多,我們會發現我們不知道的也越來越多。加油,共勉!

 


 

Java 極客技術公眾號,是由一群熱愛 Java 開發的技術人組建成立,專注分享原創、高質量的 Java 文章。如果您覺得我們的文章還不錯,請幫忙讚賞、在看、轉發支援,鼓勵我們分享出更好的文章。

關注公眾號,大家可以在公眾號後臺回覆“部落格園”,免費獲得作者 Java 知識體系/面試必看資料。

 

相關文章