Java 併發編程 ③ - ThreadLocal 和 InheritableThreadLocal 詳解

原文地址: Java 併發編程 ③ - ThreadLocal 和 InheritableThreadLocal 詳解

轉載請註明出處!java

前言

往期文章:git

繼上一篇結尾講的,這一篇文章主要是講ThreadLocal 和 InheritableThreadLocal。主要內容有:github

  • ThreadLocal 使用 和 實現原理
  • ThreadLocal 反作用編程

    • 髒數據
    • 內存泄漏的分析
  • InheritableThreadLocal 使用 和 實現原理

1、ThreadLocal

ThreadLocal 適用於每一個線程須要本身獨立的實例且該實例須要在多個方法中被使用,即變量在線程間隔離而在方法或類間共享的場景。 確切的來講,ThreadLocal 並非專門爲了解決多線程共享變量產生的併發問題而出來的,而是給提供了一個新的思路,曲線救國。數組

經過實例代碼來簡單演示下ThreadLocal的使用。安全

public class ThreadLocalExample {

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

    public static void main(String[] args) {

        ExecutorService service = Executors.newCachedThreadPool();

        service.execute(() -> {
            System.out.println(Thread.currentThread().getName() + " set 1");
            threadLocal.set(1);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 不會收到線程2的影響,由於ThreadLocal 線程本地存儲
            System.out.println(Thread.currentThread().getName() + " get " + threadLocal.get());
            threadLocal.remove();
        });

        service.execute(() -> {
            System.out.println(Thread.currentThread().getName() + " set 2");
            threadLocal.set(2);
            threadLocal.remove();
        });

        ThreadPoolUtil.tryReleasePool(service);
    }
}

能夠看到,線程1不會受到線程2的影響,由於ThreadLocal 建立的是線程私有的變量。session

2、ThreadLocal 實現原理 ⭐

2.1 理清 ThreadLocal 中幾個關鍵類之間的關係

咱們先看下ThreadLocal 與 Thread 的類圖,瞭解他們的主要方法和相互之間的關係。多線程

圖中幾個類咱們標註一下:併發

  • Thread
  • ThreadLocal
  • ThreadLocalMap
  • ThreadLocalMap.Entry

接下去,咱們首先先開始瞭解這幾個類的相互關係:異步

  1. Thread 類中有一個 threadLocals 成員變量(實際上還有一個inheritableThreadLocals,後面講),它的類型是ThreadLocal 的內部靜態類ThreadLocalMap

    public class Thread implements Runnable {
      
          // ...... 省略
          
        /* ThreadLocal values pertaining to this thread. This map is maintained
ThreadLocal.ThreadLocalMap threadLocals = null;
 
 ```
  1. ThreadLocalMap 是一個定製化的Hashmap,爲何是個HashMap?很好理解,每一個線程能夠關聯多個ThreadLocal變量。

    /**
         * ThreadLocalMap is a customized hash map suitable only for
         * maintaining thread local values. No operations are exported
         * outside of the ThreadLocal class. The class is package private to
         * allow declaration of fields in class Thread.  To help deal with
         * very large and long-lived usages, the hash table entries use
         * WeakReferences for keys. However, since reference queues are not
         * used, stale entries are guaranteed to be removed only when
*/
     static class ThreadLocalMap {
         // ...
     }
 ```
  1. ThreadLocalMap 初始化時會建立一個大小爲16的Entry 數組,Entry 對象也是用來保存 key- value 鍵值對(這個Key固定是ThreadLocal 類型)。值得注意的是,這個Entry 繼承了 WeakReference(這個設計是爲了防止內存泄漏,後面會講)

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

2.2 ThreadLocal的set、get及remove方法的源碼

a. void set(T value)

public void set(T value) {
        // ① 獲取當前線程
        Thread t = Thread.currentThread();
        // ② 去查找對應線程的ThreadLocalMap變量
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            // ③ 第一次調用就建立當前線程的對應的ThreadLocalMap
            // 而且會將值保存進去,key是當前的threadLocal,value就是傳進來的值
            createMap(t, value);
    }

    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

b. T get()

public T get() {
        // ① 獲取當前線程
        Thread t = Thread.currentThread();
        // ② 去查找對應線程的ThreadLocalMap變量
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            // ③ 不爲null,返回當前threadLocal 對應的value值
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        // ④ 當前線程的threadLocalMap爲空,初始化
        return setInitialValue();
    }

    private T setInitialValue() {
        // ⑤ 初始化的值爲null
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            // 初始化當前線程的threadLocalMap
            createMap(t, value);
        return value;
    }

    protected T initialValue() {
        return null;
    }

c. void remove()

若是當前線程的threadLocals變量不爲空,則刪除當前線程中指定ThreadLocal實例對應的本地變量。

public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }

從源碼中能夠看出來,自始至終,這些本地變量不是存放在ThreadLocal實例裏面,而是存放在調用線程的threadLocals變量,那個線程私有的threadLocalMap 裏面

ThreadLocal就是一個工具殼和一個key,它經過set方法把value值放入調用線程的threadLocals裏面並存放起來,當調用線程調用它的get方法時,再從當前線程的threadLocals變量裏面將其拿出來使用。

講到這裏,實現原理就算講完了,實際上ThreadLocal 的源碼算是很是簡單易懂。關於ThreadLocal 真正的重點和難點,是咱們後面的內容。

3、ThreadLocal 反作用

ThreadLocal 是爲了線程可以安全的共享/傳遞某個變量設計的,可是有必定的反作用。

ThreadLocal 的主要問題是會產生髒數據內存泄露

先說一個結論,這兩個問題一般是在線程池的線程中使用 ThreadLocal 引起的,由於線程池有線程複用內存常駐兩個特色。

3.1 髒數據

髒數據應該是你們比較好理解的,因此這裏呢,先拿出來說。線程複用會產生髒數據。因爲線程池會重用 Thread 對象 ,那麼與 Thread 綁定的類的靜態屬性 ThreadLocal 變量也會被重用。若是在實現的線程 run() 方法體中不顯式地調用 remove() 清理與線程相關的 ThreadLocal 信息,那麼假若下一個線程不調用 set() 設置初始值,就可能 get() 到重用的線程信息,包括 ThreadLocal 所關聯的線程對象的 value 值。

爲了便於理解,這裏提供一個demo:

public class ThreadLocalDirtyDataDemo {

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

    public static void main(String[] args) {

        ExecutorService pool = Executors.newFixedThreadPool(1);

        for (int i = 0; i < 2; i++) {
            MyThread thread = new MyThread();
            pool.execute(thread);
        }
        ThreadPoolUtil.tryReleasePool(pool);
    }

    private static class MyThread extends Thread {
        private static boolean flag = true;

        @Override
        public void run() {
            if (flag) {
                // 第一個線程set以後,並無進行remove
                // 而第二個線程因爲某種緣由(這裏是flag=false) 沒有進行set操做
                String sessionInfo = this.getName();
                threadLocal.set(sessionInfo);
                flag = false;
            }
            System.out.println(this.getName() + " 線程 是 " + threadLocal.get());
            // 線程使用完threadLocal,要及時remove,這裏是爲了演示錯誤狀況
        }
    }
}

執行結果:

Thread-0 線程 是 Thread-0
Thread-1 線程 是 Thread-0

3.2 內存泄露 ⭐

在講這個以前,有必要看一張圖,從棧與堆的角度看看ThreadLocal 使用過程中幾個類的引用關係。

看到紅色的虛線箭頭沒?這個就是理解ThreadLocal的一個重點和難點。

咱們再看一遍Entry的源碼:

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

ThreadLocalMap 的每一個 Entry 都是一個對的弱引用 - WeakReference<ThreadLocal<?>>,這一點從super(k)可看出。另外,每一個 Entry都包含了一個對 的強引用。

在前面的敘述中,我有提到Entry extends WeakReference<ThreadLocal<?>> 是爲了防止內存泄露。實際上,這裏說的防止內存泄露是針對ThreadLocal 對象的

怎麼說呢?繼續往下看。

若是你有學習過Java 中的引用的話,這個WeakReference應該不會陌生,當 JVM 進行垃圾回收時,不管內存是否充足,都會回收<font color='#ff6600' >只被弱引用關聯</font>的對象。

更詳細的相關內容能夠閱讀筆者的這篇文章 【萬字精美圖文帶你掌握JVM垃圾回收#Java 中的引用】 )

經過這種設計,即便線程正在執行中, 只要 ThreadLocal 對象引用被置成 null,Entry 的 Key 就會自動在下一次 YGC 時被垃圾回收(由於只剩下ThreadLocalMap 對其的弱引用,沒有強引用了)

若是這裏Entry 的key 值是對 ThreadLocal 對象的強引用的話,那麼 即便ThreadLocal的對象引用被聲明成null 時,這些 ThreadLocal 不能被回收,由於還有來自 ThreadLocalMap 的強引用,這樣子就會形成內存泄漏

這類key被回收( key == null)的Entry 在 ThreadLocalMap 源碼中被稱爲 stale entry (翻譯過來就是 「過期的條目」),會在下一次執行 ThreadLocalMap 的 getEntry 和 set 方法中,將 這些 stale entry 的value 置爲 null,使得原來value 指向的變量能夠被垃圾回收

「會在下一次執行 ThreadLocalMap 的 getEntry 和 set 方法中,將 這些 stale entry 的value 置爲 null,使得 原來value 指向的變量能夠被垃圾回收」這一部分描述,能夠查閱 ThreadLocalMap#expungeStaleEntry()方法源碼及調用這個方法的地方。

這樣子來看,ThreadLocalMap 是經過這種設計,解決了 ThreadLocal 對象可能會存在的內存泄漏的問題而且對應的value 也會由於上述的 stale entry 機制被垃圾回收


可是咱們爲何還會說使用ThreadLocal 可能存在內存泄露問題呢,在這裏呢,指的是還存在那個Value(圖中的紫色塊)實例沒法被回收的狀況

請注意哦,上述機制的前提是ThreadLocal 的引用被置爲null,纔會觸發弱引用機制,繼而回收Entry 的 Value對象實例。咱們來看下ThreadLocal 源碼中的註釋

instances are typically private static fields in classes

ThreadLocal 對象一般做爲私有靜態變量使用

-- 若是說一個 ThreadLocal 是非靜態的,屬於某個線程實例類,那就失去了線程內共享的本質屬性。

做爲靜態變量使用的話, 那麼其生命週期至少不會隨着線程結束而結束。也就是說,絕大多數的靜態threadLocal對象都不會被置爲null。這樣子的話,經過 stale entry 這種機制來清除Value 對象實例這條路是走不通的。必需要手動remove() 才能保證。

這裏仍是用上面的例子來作示例。

public class ThreadLocalDirtyDataDemo {

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

    public static void main(String[] args) {

        ExecutorService pool = Executors.newFixedThreadPool(1);

        for (int i = 0; i < 2; i++) {
            MyThread thread = new MyThread();
            pool.execute(thread);
        }
        ThreadPoolUtil.tryReleasePool(pool);
    }

    private static class MyThread extends Thread {
        private static boolean flag = true;

        @Override
        public void run() {
            if (flag) {
                // 第一個線程set以後,並無進行remove
                // 而第二個線程因爲某種緣由(這裏是flag=false) 沒有進行set操做
                String sessionInfo = this.getName();
                threadLocal.set(sessionInfo);
                flag = false;
            }
            System.out.println(this.getName() + " 線程 是 " + threadLocal.get());
            // 線程使用完threadLocal,要及時remove,這裏是爲了演示錯誤狀況
        }
    }
}

在這個例子當中,若是不進行 remove() 操做, 那麼這個線程執行完成後,經過 ThreadLocal 對象持有的 String 對象是不會被釋放的。

爲何說只有線程複用的時候,會出現這個問題呢?固然啦,由於這些本地變量都是存儲在線程的內部變量中的,當線程銷燬時,threadLocalMap的對象引用會被置爲null,value實例對象 隨着線程的銷燬,在內存中成爲了避免可達對象,而後被垃圾回收。

// Thread#exit()
    private void exit() {
        if (group != null) {
            group.threadTerminated(this);
            group = null;
        }
        /* Aggressively null out all reference fields: see bug 4006245 */
        target = null;
        /* Speed the release of some of these resources */
        threadLocals = null;
        inheritableThreadLocals = null;
        inheritedAccessControlContext = null;
        blocker = null;
        uncaughtExceptionHandler = null;
    }

總結

總結一下

  • WeakReference 的引入,是爲了將ThreadLocal 對象與ThreadLocalMap 設計成一種弱引用的關係,來避免ThreadLocal 實例對象不能被回收而存在的內存泄露問題,當threadLocal 對象被回收時,會有清理 stale entry 機制,回收其對應的Value實例對象。
  • 咱們常說的內存泄露問題,針對的是threadLocal對應的Value對象實例。在線程對象被重用且threadLocal爲靜態變量時,若是沒有手動remove(),就可能會形成內存泄露的狀況。
  • 上述兩種內存泄露的狀況只有在線程複用的狀況下才會出現,由於在線程銷燬時threadLocalMap的對象引用會被置爲null。
  • 解決反作用的方法很簡單,就是每次用完ThreadLocal,都要及時調用 remove() 方法去清理。

4、InheritableThreadLocal

在一些場景中,子線程須要能夠獲取父線程的本地變量,好比用一個統一的ID來追蹤記錄調用鏈路。可是ThreadLocal 是不支持繼承性的,同一個ThreadLocal變量在父線程中被設置值後,在子線程中是獲取不到對應的對象的。

爲了解決這個問題,InheritableThreadLocal 也就應運而生。

4.1 使用

public class InheritableThreadLocalDemo {

    private static ThreadLocal<String> threadLocal = new InheritableThreadLocal<>();

    public static void main(String[] args) {
        // 主線程
        threadLocal.set("hello world");
        // 啓動子線程
        Thread thread = new Thread(() -> {
            // 子線程輸出父線程的threadLocal 變量值
            System.out.println("子線程: " + threadLocal.get());
        });

        thread.start();

        System.out.println("main: " +threadLocal.get());

    }
}

輸出:

main: hello world
子線程: hello world

4.2 原理

要了解原理,咱們先來看一下 InheritableThreadLocal 的源碼。

public class InheritableThreadLocal<T> extends ThreadLocal<T> {

    // ①
    protected T childValue(T parentValue) {
        return parentValue;
    }

    // ②
    ThreadLocalMap getMap(Thread t) {
       return t.inheritableThreadLocals;
    }

    // ③
    void createMap(Thread t, T firstValue) {
        t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
    }
}
public class Thread implements Runnable {

    /*
     * InheritableThreadLocal values pertaining to this thread. This map is
     * maintained by the InheritableThreadLocal class.
     */
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

能夠看到,InheritableThreadLocal 繼承了ThreadLocal,而且重寫了三個方法,看來實現的門道就在這三個方法裏面。

先看代碼③,InheritableThreadLocal 重寫了createMap方法,那麼如今當第一次調用set方法時,建立的是當前線程的inheritableThreadLocals 變量的實例而再也不是threadLocals。由代碼②可知,當調用get方法獲取當前線程內部的map變量時,獲取的是inheritableThreadLocals而再也不是threadLocals。

能夠這麼說,在InheritableThreadLocal的世界裏,變量inheritableThreadLocals替代了threadLocals。

代碼②③都講了,再來看看代碼①,以及如何讓子線程能夠訪問父線程的本地變量。

這要從建立Thread的代碼提及,打開Thread類的默認構造函數,代碼以下。

public Thread(Runnable target) {
        init(null, target, "Thread-" + nextThreadNum(), 0);
    }

    private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals) {
        
        // ... 省略無關部分
        // 獲取父線程 - 當前線程
        Thread parent = currentThread();
        
        // ... 省略無關部分
        // 若是父線程的inheritThreadLocals不爲null 且 inheritThreadLocals=true
        if (inheritThreadLocals && parent.inheritableThreadLocals != null)
            // 設置子線程中的inheritableThreadLocals變量
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
        // ... 省略無關部分
    }

    static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
        return new ThreadLocalMap(parentMap);
    }

再來看看裏面是如何執行createInheritedMap 的。

private ThreadLocalMap(ThreadLocalMap parentMap) {
            Entry[] parentTable = parentMap.table;
            int len = parentTable.length;
            setThreshold(len);
            table = new Entry[len];

            for (int j = 0; j < len; j++) {
                Entry e = parentTable[j];
                if (e != null) {
                    @SuppressWarnings("unchecked")
                    ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
                    if (key != null) {
                        // 這裏調用了重寫的代碼① childValue
                        Object value = key.childValue(e.value);
                        Entry c = new Entry(key, value);
                        int h = key.threadLocalHashCode & (len - 1);
                        while (table[h] != null)
                            h = nextIndex(h, len);
                        table[h] = c;
                        size++;
                    }
                }
            }
        }

在該構造函數內部把父線程的inheritableThreadLocals成員變量的值複製到新的ThreadLocalMap 對象中。

## 小結

本章講了ThreadLocal 和 InheritableThreadLocal 的相關知識點。

ThreadLocal 實現線程內部變量共享,InheritableThreadLocal 實現了父線程與子線程的變量繼承。可是還有一種場景,InheritableThreadLocal 沒法解決,也就是在使用線程池等會池化複用線程的執行組件狀況下,異步執行執行任務,須要傳遞上下文的狀況

針對上述狀況,阿里開源了一個TTL,即Transmittable ThreadLocal來解決這個問題,有興趣的朋友們能夠去看看。

以後有時間的話我會單獨寫一篇文章介紹一下。

若是本文有幫助到你,但願能點個贊,這是對個人最大動力🤝🤝🤗🤗。

參考

  • 《Java 併發編程之美》
  • 《碼出高效》
相關文章
相關標籤/搜索